From e94da7ca41c817ac96373232f9da3448c235a330 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Oct 2018 22:02:23 +0200 Subject: [PATCH 01/76] inverse backtest logic to loop over time - not pairs (more realistic) --- freqtrade/optimize/backtesting.py | 40 +++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6fcde64fa..212c675fd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -66,6 +66,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] for strat in list(self.config['strategy_list']): stratconf = deepcopy(self.config) stratconf['strategy'] = strat @@ -86,6 +87,8 @@ 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.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe self.advise_buy = strategy.advise_buy self.advise_sell = strategy.advise_sell @@ -280,8 +283,13 @@ class Backtesting(object): processed = args['processed'] max_open_trades = args.get('max_open_trades', 0) position_stacking = args.get('position_stacking', False) + start_date = args.get('start_date') + end_date = args.get('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 @@ -296,15 +304,29 @@ class Backtesting(object): # Convert from Pandas to list for performance reasons # (Looping Pandas is slow.) - ticker = [x for x in ticker_data.itertuples()] + ticker[pair] = [x for x in ticker_data.itertuples()] + pairs.append(pair) + + lock_pair_until: 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): + try: + row = ticker[pair][index] + except IndexError: + # missing Data for one pair ... + # TODO:howto handle this + # logger.warning(f"i: {index} - {tmp} did not exist for {pair}") + continue - lock_pair_until = None - for index, row in enumerate(ticker): 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 lock_pair_until is not None and row.date <= lock_pair_until: + if 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 @@ -313,17 +335,19 @@ 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[index + 1:], + trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][index + 1:], trade_count_lock, args) if trade_entry: - lock_pair_until = trade_entry.close_time + lock_pair_until[pair] = trade_entry.close_time 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 = ticker_data.iloc[-1].date + lock_pair_until[pair] = end_date + tmp += timedelta(minutes=self.ticker_interval_mins) + index += 1 return DataFrame.from_records(trades, columns=BacktestResult._fields) def start(self) -> None: @@ -390,6 +414,8 @@ class Backtesting(object): 'processed': preprocessed, 'max_open_trades': max_open_trades, 'position_stacking': self.config.get('position_stacking', False), + 'start_date': min_date, + 'end_date': max_date, } ) From 96efd12a312c14afae0034c4dae2d99972059e6f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Oct 2018 19:35:16 +0200 Subject: [PATCH 02/76] add new options to hyperopt --- freqtrade/optimize/hyperopt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b2d05d603..9766f5acb 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -276,11 +276,14 @@ class Hyperopt(Backtesting): self.strategy.stoploss = params['stoploss'] processed = load(TICKERDATA_PICKLE) + min_date, max_date = Backtesting.get_timeframe(processed) results = self.backtest( { 'stake_amount': self.config['stake_amount'], 'processed': processed, 'position_stacking': self.config.get('position_stacking', True), + 'start_date': min_date, + 'end_date': max_date, } ) result_explanation = self.format_results(results) From 6729dfa6d3ed8fec31f371f936f475184256b5dd Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Oct 2018 20:32:33 +0200 Subject: [PATCH 03/76] Add get_timeframe mock for hyperopt --- freqtrade/tests/optimize/test_hyperopt.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index c93f2d316..3ea374bf4 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 +from datetime import datetime import os from unittest.mock import MagicMock @@ -291,6 +292,10 @@ def test_generate_optimizer(mocker, default_conf) -> None: 'freqtrade.optimize.hyperopt.Hyperopt.backtest', MagicMock(return_value=backtest_result) ) + mocker.patch( + 'freqtrade.optimize.backtesting.Backtesting.get_timeframe', + MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) + ) patch_exchange(mocker) mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock()) From 03cda8e23eccbeccf8f66d618605d2413b3c33f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Oct 2018 20:09:12 +0200 Subject: [PATCH 04/76] remove meaningless backtesting test --- freqtrade/tests/optimize/test_backtesting.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 20f2a6582..2d2b2d4a7 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -449,7 +449,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: ) default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] - default_conf['ticker_interval'] = "1m" + default_conf['ticker_interval'] = '1m' default_conf['live'] = False default_conf['datadir'] = None default_conf['export'] = None @@ -587,21 +587,6 @@ def test_backtest_pricecontours(default_conf, fee, mocker) -> None: simple_backtest(default_conf, contour, numres, mocker) -# Test backtest using offline data (testdata directory) -def test_backtest_ticks(default_conf, fee, mocker): - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - patch_exchange(mocker) - ticks = [1, 5] - fun = Backtesting(default_conf).advise_buy - for _ in ticks: - backtest_conf = _make_backtest_conf(mocker, conf=default_conf) - backtesting = Backtesting(default_conf) - backtesting.advise_buy = fun # Override - backtesting.advise_sell = fun # Override - results = backtesting.backtest(backtest_conf) - assert not results.empty - - def test_backtest_clash_buy_sell(mocker, default_conf): # Override the default buy trend function in our default_strategy def fun(dataframe=None, pair=None): From db17ccef2b9e89982d20cbfe11623f47a702095d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Oct 2018 20:34:20 +0200 Subject: [PATCH 05/76] Adapt backtesting-tests to new backtest-logic --- freqtrade/tests/optimize/test_backtesting.py | 32 +++++++++++++++----- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 2d2b2d4a7..e3e078d72 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -86,17 +86,21 @@ def load_data_test(what): def simple_backtest(config, contour, num_results, mocker) -> None: patch_exchange(mocker) + config['ticker_interval'] = '1m' backtesting = Backtesting(config) data = load_data_test(contour) processed = backtesting.strategy.tickerdata_to_dataframe(data) + min_date, max_date = Backtesting.get_timeframe(processed) assert isinstance(processed, dict) results = backtesting.backtest( { 'stake_amount': config['stake_amount'], 'processed': processed, 'max_open_trades': 1, - 'position_stacking': False + 'position_stacking': False, + 'start_date': min_date, + 'end_date': max_date, } ) # results :: @@ -123,12 +127,16 @@ def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None): data = trim_dictlist(data, -201) patch_exchange(mocker) backtesting = Backtesting(conf) + processed = backtesting.strategy.tickerdata_to_dataframe(data) + min_date, max_date = Backtesting.get_timeframe(processed) return { 'stake_amount': conf['stake_amount'], - 'processed': backtesting.strategy.tickerdata_to_dataframe(data), + 'processed': processed, 'max_open_trades': 10, 'position_stacking': False, - 'record': record + 'record': record, + 'start_date': min_date, + 'end_date': max_date, } @@ -505,12 +513,15 @@ def test_backtest(default_conf, fee, mocker) -> None: data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC']) data = trim_dictlist(data, -200) data_processed = backtesting.strategy.tickerdata_to_dataframe(data) + min_date, max_date = Backtesting.get_timeframe(data_processed) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], 'processed': data_processed, 'max_open_trades': 10, - 'position_stacking': False + 'position_stacking': False, + 'start_date': min_date, + 'end_date': max_date, } ) assert not results.empty @@ -554,12 +565,16 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None: # Run a backtesting for an exiting 5min ticker_interval data = optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC']) data = trim_dictlist(data, -200) + processed = backtesting.strategy.tickerdata_to_dataframe(data) + min_date, max_date = Backtesting.get_timeframe(processed) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], - 'processed': backtesting.strategy.tickerdata_to_dataframe(data), + 'processed': processed, 'max_open_trades': 1, - 'position_stacking': False + 'position_stacking': False, + 'start_date': min_date, + 'end_date': max_date, } ) assert not results.empty @@ -582,7 +597,10 @@ def test_processed(default_conf, mocker) -> None: def test_backtest_pricecontours(default_conf, fee, mocker) -> None: mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - tests = [['raise', 18], ['lower', 0], ['sine', 19]] + tests = [['raise', 18], ['lower', 0], ['sine', 16]] + # We need to enable sell-signal - otherwise it sells on ROI!! + default_conf['experimental'] = {"use_sell_signal": True} + for [contour, numres] in tests: simple_backtest(default_conf, contour, numres, mocker) From 83a8d79115b1056c1469338da4965817243e33f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Oct 2018 20:35:08 +0200 Subject: [PATCH 06/76] Fix alternate buy/sell (this should respect the sell signal!) --- freqtrade/tests/optimize/test_backtesting.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index e3e078d72..20c117a8e 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -638,14 +638,16 @@ def test_backtest_only_sell(mocker, default_conf): def test_backtest_alternate_buy_sell(default_conf, fee, mocker): mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC') + # We need to enable sell-signal - otherwise it sells on ROI!! + default_conf['experimental'] = {"use_sell_signal": True} backtesting = Backtesting(default_conf) backtesting.advise_buy = _trend_alternate # Override backtesting.advise_sell = _trend_alternate # Override results = backtesting.backtest(backtest_conf) backtesting._store_backtest_result("test_.json", results) - assert len(results) == 4 + assert len(results) == 21 # One trade was force-closed at the end - assert len(results.loc[results.open_at_end]) == 1 + assert len(results.loc[results.open_at_end]) == 0 def test_backtest_record(default_conf, fee, mocker): From 66487f2a133decf3ef887f732fc58b1927c9079a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Oct 2018 20:36:24 +0200 Subject: [PATCH 07/76] require start/end-date argument in backtest --- freqtrade/optimize/backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 212c675fd..49f6375f7 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -283,8 +283,8 @@ class Backtesting(object): processed = args['processed'] max_open_trades = args.get('max_open_trades', 0) position_stacking = args.get('position_stacking', False) - start_date = args.get('start_date') - end_date = args.get('end_date') + start_date = args['start_date'] + end_date = args['end_date'] trades = [] trade_count_lock: Dict = {} ticker: Dict = {} From 2371d1e696b3a6491cbc2883b0fa523b491b9896 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Oct 2018 06:54:07 +0200 Subject: [PATCH 08/76] Fix backtest test (don't use 8m file if we use 1m tickers) --- freqtrade/tests/optimize/test_backtesting.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 20c117a8e..d0c8a88aa 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -640,12 +640,16 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker): backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC') # We need to enable sell-signal - otherwise it sells on ROI!! default_conf['experimental'] = {"use_sell_signal": True} + default_conf['ticker_interval'] = '1m' backtesting = Backtesting(default_conf) backtesting.advise_buy = _trend_alternate # Override backtesting.advise_sell = _trend_alternate # Override results = backtesting.backtest(backtest_conf) backtesting._store_backtest_result("test_.json", results) - assert len(results) == 21 + # 200 candles in backtest data + # won't buy on first (shifted by 1) + # 100 buys signals + assert len(results) == 99 # One trade was force-closed at the end assert len(results.loc[results.open_at_end]) == 0 From fa4c199aa63d13837688ed0b7f3fa2d0a7d627a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Nov 2018 13:23:59 +0100 Subject: [PATCH 09/76] fix some mismatches after rebase --- freqtrade/tests/optimize/test_backtesting.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index d0c8a88aa..b0d4f2cbd 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -13,6 +13,7 @@ from arrow import Arrow from freqtrade import DependencyException, constants, optimize from freqtrade.arguments import Arguments, TimeRange +from freqtrade.optimize import get_timeframe from freqtrade.optimize.backtesting import (Backtesting, setup_configuration, start) from freqtrade.tests.conftest import log_has, patch_exchange @@ -91,7 +92,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None: data = load_data_test(contour) processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = Backtesting.get_timeframe(processed) + min_date, max_date = get_timeframe(processed) assert isinstance(processed, dict) results = backtesting.backtest( { @@ -128,7 +129,7 @@ def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None): patch_exchange(mocker) backtesting = Backtesting(conf) processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = Backtesting.get_timeframe(processed) + min_date, max_date = get_timeframe(processed) return { 'stake_amount': conf['stake_amount'], 'processed': processed, @@ -513,7 +514,7 @@ def test_backtest(default_conf, fee, mocker) -> None: data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC']) data = trim_dictlist(data, -200) data_processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = Backtesting.get_timeframe(data_processed) + min_date, max_date = get_timeframe(data_processed) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], @@ -566,7 +567,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None: data = optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC']) data = trim_dictlist(data, -200) processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = Backtesting.get_timeframe(processed) + min_date, max_date = get_timeframe(processed) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], From 9cd2ed5a16ae7ed5c617dd345ade46d7b8d6d18c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Nov 2018 13:43:09 +0100 Subject: [PATCH 10/76] fix hyperopt get_timeframe mock --- freqtrade/optimize/hyperopt.py | 4 ++-- freqtrade/tests/optimize/test_hyperopt.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 9766f5acb..edf73fcf3 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -23,7 +23,7 @@ from skopt.space import Categorical, Dimension, Integer, Real import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration -from freqtrade.optimize import load_data +from freqtrade.optimize import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting logger = logging.getLogger(__name__) @@ -276,7 +276,7 @@ class Hyperopt(Backtesting): self.strategy.stoploss = params['stoploss'] processed = load(TICKERDATA_PICKLE) - min_date, max_date = Backtesting.get_timeframe(processed) + min_date, max_date = get_timeframe(processed) results = self.backtest( { 'stake_amount': self.config['stake_amount'], diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 3ea374bf4..703b88fc1 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock import pandas as pd import pytest -from freqtrade.optimize.__init__ import load_tickerdata_file +from freqtrade.optimize import load_tickerdata_file from freqtrade.optimize.hyperopt import Hyperopt, start from freqtrade.strategy.resolver import StrategyResolver from freqtrade.tests.conftest import log_has, patch_exchange @@ -293,7 +293,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: MagicMock(return_value=backtest_result) ) mocker.patch( - 'freqtrade.optimize.backtesting.Backtesting.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) patch_exchange(mocker) From 93429a58b2d605215b4157cb8409cb845ded97ed Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Nov 2018 13:45:22 +0100 Subject: [PATCH 11/76] remove TODO --- freqtrade/optimize/backtesting.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 49f6375f7..4dc3119d3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -318,8 +318,7 @@ class Backtesting(object): row = ticker[pair][index] except IndexError: # missing Data for one pair ... - # TODO:howto handle this - # logger.warning(f"i: {index} - {tmp} did not exist for {pair}") + # Warnings for this are shown by `validate_backtest_data` continue if row.buy == 0 or row.sell == 1: From 56dcf080a906645df2a2872618c3666c129d8973 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Nov 2018 20:03:04 +0100 Subject: [PATCH 12/76] Add explicit test for parallel trades --- freqtrade/tests/optimize/test_backtesting.py | 71 ++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index b0d4f2cbd..e03e011f3 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -655,6 +655,77 @@ 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): + + def evaluate_result_multi(results, freq, max_open_trades): + # Find overlapping trades by expanding each trade once per period + # and then counting overlaps + dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time, freq=freq)) + for row in results[['open_time', 'close_time']].iterrows()] + deltas = [len(x) for x in dates] + 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() + return df_final[df_final['pair'] > max_open_trades] + + def _trend_alternate_hold(dataframe=None, metadata=None): + """ + Buy every 8th candle - sell every other 8th -2 (hold on to pairs a bit) + """ + multi = 8 + 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) + pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC'] + data = optimize.load_data(None, ticker_interval='5m', pairs=pairs) + data = trim_dictlist(data, -500) + # We need to enable sell-signal - otherwise it sells on ROI!! + default_conf['experimental'] = {"use_sell_signal": True} + default_conf['ticker_interval'] = '5m' + + backtesting = Backtesting(default_conf) + backtesting.advise_buy = _trend_alternate_hold # Override + backtesting.advise_sell = _trend_alternate_hold # Override + + data_processed = backtesting.strategy.tickerdata_to_dataframe(data) + min_date, max_date = get_timeframe(data_processed) + backtest_conf = { + 'stake_amount': default_conf['stake_amount'], + 'processed': data_processed, + 'max_open_trades': 3, + 'position_stacking': False, + 'start_date': min_date, + 'end_date': max_date, + } + + results = backtesting.backtest(backtest_conf) + + # Make sure we have parallel trades + assert len(evaluate_result_multi(results, '5min', 2)) > 0 + # make sure we don't have trades with more than configured max_open_trades + assert len(evaluate_result_multi(results, '5min', 3)) == 0 + + backtest_conf = { + 'stake_amount': default_conf['stake_amount'], + 'processed': data_processed, + 'max_open_trades': 1, + 'position_stacking': False, + 'start_date': min_date, + 'end_date': max_date, + } + results = backtesting.backtest(backtest_conf) + assert len(evaluate_result_multi(results, '5min', 1)) == 0 + + def test_backtest_record(default_conf, fee, mocker): names = [] records = [] From 272ff51d512d0c06832812f985744a7cbea72719 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Nov 2018 20:37:59 +0100 Subject: [PATCH 13/76] correctly patch exchange --- freqtrade/tests/optimize/test_backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index e03e011f3..aa1f144cd 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -685,6 +685,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker): return dataframe mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + patch_exchange(mocker) pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC'] data = optimize.load_data(None, ticker_interval='5m', pairs=pairs) data = trim_dictlist(data, -500) From 5c5fe4c13a4b92be65e0e52903bb804f0e397f09 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Nov 2018 07:14:43 +0100 Subject: [PATCH 14/76] Fix test --- 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 aa1f144cd..03d874f5f 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -598,7 +598,7 @@ def test_processed(default_conf, mocker) -> None: def test_backtest_pricecontours(default_conf, fee, mocker) -> None: mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - tests = [['raise', 18], ['lower', 0], ['sine', 16]] + tests = [['raise', 18], ['lower', 0], ['sine', 19]] # We need to enable sell-signal - otherwise it sells on ROI!! default_conf['experimental'] = {"use_sell_signal": True} From 292962d64dfb88d9d0b4b55aab9f5883577867b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Nov 2018 19:34:18 +0100 Subject: [PATCH 15/76] Fix tests --- freqtrade/tests/optimize/__init__.py | 5 +++-- freqtrade/tests/optimize/test_backtest_detail.py | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/freqtrade/tests/optimize/__init__.py b/freqtrade/tests/optimize/__init__.py index 2b7222e88..58ea7c343 100644 --- a/freqtrade/tests/optimize/__init__.py +++ b/freqtrade/tests/optimize/__init__.py @@ -4,9 +4,10 @@ import arrow from pandas import DataFrame from freqtrade.strategy.interface import SellType +from freqtrade.constants import TICKER_INTERVAL_MINUTES ticker_start_time = arrow.get(2018, 10, 3) -ticker_interval_in_minute = 60 +tests_ticker_interval = "1h" class BTrade(NamedTuple): @@ -31,7 +32,7 @@ class BTContainer(NamedTuple): def _get_frame_time_from_offset(offset): return ticker_start_time.shift( - minutes=(offset * ticker_interval_in_minute)).datetime + minutes=(offset * TICKER_INTERVAL_MINUTES[tests_ticker_interval])).datetime def _build_backtest_dataframe(ticker_with_signals): diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index 806c136bc..7db6913f3 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -6,10 +6,11 @@ from pandas import DataFrame import pytest +from freqtrade.optimize 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) + _get_frame_time_from_offset, tests_ticker_interval) from freqtrade.tests.conftest import patch_exchange @@ -147,6 +148,7 @@ 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)) patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) @@ -158,11 +160,14 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: pair = 'UNITTEST/BTC' # Dummy data as we mock the analyze functions data_processed = {pair: DataFrame()} + min_date, max_date = get_timeframe({pair: frame}) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], 'processed': data_processed, 'max_open_trades': 10, + 'start_date': min_date, + 'end_date': max_date, } ) print(results.T) From 59cd4fe0ef735bb18d2d3ab484616921670176c0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Nov 2018 19:34:46 +0100 Subject: [PATCH 16/76] Remove boilerplate comments --- freqtrade/tests/optimize/test_backtest_detail.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index 7db6913f3..e5849319e 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -174,18 +174,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: assert len(results) == len(data.trades) assert round(results["profit_percent"].sum(), 3) == round(data.profit_perc, 3) - # if data.sell_r == SellType.STOP_LOSS: - # assert log_has("Stop loss hit.", caplog.record_tuples) - # else: - # assert not log_has("Stop loss hit.", caplog.record_tuples) - # log_test = (f'Force_selling still open trade UNITTEST/BTC with ' - # f'{results.iloc[-1].profit_percent} perc - {results.iloc[-1].profit_abs}') - # if data.sell_r == SellType.FORCE_SELL: - # assert log_has(log_test, - # caplog.record_tuples) - # else: - # assert not log_has(log_test, - # caplog.record_tuples) + for c, trade in enumerate(data.trades): res = results.iloc[c] assert res.sell_reason == trade.sell_reason From b50250139ec3171309ad62dd784b159e965245b9 Mon Sep 17 00:00:00 2001 From: misagh Date: Mon, 19 Nov 2018 20:02:26 +0100 Subject: [PATCH 17/76] Drafting stoploss on exchange --- freqtrade/strategy/interface.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 212559c8c..d1d4703a4 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -67,6 +67,11 @@ class IStrategy(ABC): # associated stoploss stoploss: float + # if the stoploss should be on exchange. + # if this is True then a stoploss order will be placed + # immediately after a successful buy order. + stoploss_on_exchange: bool = False + # associated ticker interval ticker_interval: str @@ -214,7 +219,11 @@ class IStrategy(ABC): # Set current rate to low for backtesting sell current_rate = low or rate current_profit = trade.calc_profit_percent(current_rate) - stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, + + if self.stoploss_on_exchange: + stoplossflag = False + else: + stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, force_stoploss=force_stoploss) if stoplossflag.sell_flag: From bfbdddff26bd9296da0b046f260db1ce5778b703 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 16:24:40 +0100 Subject: [PATCH 18/76] stoploss limit order added to exchange --- freqtrade/exchange/__init__.py | 18 ++++++++++++++++++ freqtrade/strategy/interface.py | 2 ++ 2 files changed, 20 insertions(+) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index ae07e36e9..59a5da23e 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -333,6 +333,24 @@ class Exchange(object): except ccxt.BaseError as e: raise OperationalException(e) + def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict: + # Only binance is supported + if not self._api.name == 'Binance': + raise NotImplementedError( + 'Stoploss limit orders are implemented only for binance as of now.') + + # 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) + stop_price = self.symbol_price_prec(pair, stop_price) + + # Ensure rate is less than stop price + if stop_price >= rate: + raise OperationalException( + 'In stoploss limit order, stop price should be more than limit price') + + return self._api.create_order(pair, 'stop_loss', 'sell', amount, rate, {'stopPrice': stop_price}) + @retrier def get_balance(self, currency: str) -> float: if self._conf['dry_run']: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 9b7b180cc..df0e3cf72 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -233,8 +233,10 @@ class IStrategy(ABC): stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, force_stoploss=force_stoploss) + if stoplossflag.sell_flag: return stoplossflag + # Set current rate to low for backtesting sell current_rate = high or rate current_profit = trade.calc_profit_percent(current_rate) From 3b7e05e07b15c1df1bfe62bfbf264feac9877784 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 16:26:24 +0100 Subject: [PATCH 19/76] stop loss order added right after a buy order is executued --- freqtrade/freqtradebot.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8a2db84a9..f1aae3c3f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -508,6 +508,37 @@ class FreqtradeBot(object): Trade.session.add(trade) Trade.session.flush() + # Check if stoploss should be added on exchange + # If True then here immediately after buy we should + # Add the stoploss order + if self.strategy.stoploss_on_exchange: + stoploss = self.edge.stoploss if self.edge else self.strategy.stoploss + stop_price = buy_limit * (1 + stoploss) + + # limit price should be less than stop price. + # 0.98 is arbitrary here. + limit_price = stop_price * 0.98 + + order_id = self.exchange.stoploss_limit(pair=pair, amount=amount, + stop_price=stop_price, rate=limit_price)['id'] + + trade = Trade( + pair=pair, + stake_amount=stake_amount, + amount=amount, + fee_open=fee, + fee_close=fee, + stoploss=stop_price, + open_date=datetime.utcnow(), + 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']] + ) + + Trade.session.add(trade) + Trade.session.flush() + # Updating wallets self.wallets.update() From bb37b56dea02fdf7e5b51e54ae7144e808ab2e08 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 16:47:52 +0100 Subject: [PATCH 20/76] adding stop loss order id to Trade --- freqtrade/persistence.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 51a8129fb..db6d526c7 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -178,6 +178,7 @@ class Trade(_DECL_BASE): # absolute value of the initial stop loss initial_stop_loss = Column(Float, nullable=True, default=0.0) # absolute value of the highest reached price + stoploss_order_id = Column(Integer, nullable=True, index=True) max_rate = Column(Float, nullable=True, default=0.0) sell_reason = Column(String, nullable=True) strategy = Column(String, nullable=True) From fad75939356188b1bf2d8ca7eb82fa5e4201ba4a Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 16:53:50 +0100 Subject: [PATCH 21/76] =?UTF-8?q?doesn=E2=80=99t=20have=20to=20create=20an?= =?UTF-8?q?other=20Trade=20for=20SL.=20can=20be=20cumulated=20into=20the?= =?UTF-8?q?=20same.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- freqtrade/freqtradebot.py | 48 ++++++++++++++------------------------- 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f1aae3c3f..f3537f2ab 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -479,6 +479,22 @@ class FreqtradeBot(object): order_id = self.exchange.buy(pair=pair, ordertype=self.strategy.order_types['buy'], amount=amount, rate=buy_limit)['id'] + stoploss_order_id: int = None + + # Check if stoploss should be added on exchange + # If True then here immediately after buy we should + # Add the stoploss order + if self.strategy.stoploss_on_exchange: + stoploss = self.edge.stoploss if self.edge else self.strategy.stoploss + stop_price = buy_limit * (1 + stoploss) + + # limit price should be less than stop price. + # 0.98 is arbitrary here. + limit_price = stop_price * 0.98 + + stoploss_order_id = self.exchange.stoploss_limit(pair=pair, amount=amount, + stop_price=stop_price, rate=limit_price)['id'] + self.rpc.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': self.exchange.name.capitalize(), @@ -502,43 +518,13 @@ class FreqtradeBot(object): open_date=datetime.utcnow(), exchange=self.exchange.id, open_order_id=order_id, + stoploss_order_id=stoploss_order_id, strategy=self.strategy.get_strategy_name(), ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']] ) Trade.session.add(trade) Trade.session.flush() - # Check if stoploss should be added on exchange - # If True then here immediately after buy we should - # Add the stoploss order - if self.strategy.stoploss_on_exchange: - stoploss = self.edge.stoploss if self.edge else self.strategy.stoploss - stop_price = buy_limit * (1 + stoploss) - - # limit price should be less than stop price. - # 0.98 is arbitrary here. - limit_price = stop_price * 0.98 - - order_id = self.exchange.stoploss_limit(pair=pair, amount=amount, - stop_price=stop_price, rate=limit_price)['id'] - - trade = Trade( - pair=pair, - stake_amount=stake_amount, - amount=amount, - fee_open=fee, - fee_close=fee, - stoploss=stop_price, - open_date=datetime.utcnow(), - 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']] - ) - - Trade.session.add(trade) - Trade.session.flush() - # Updating wallets self.wallets.update() From da5617624c97389b0a1aabd3e95e18258e6b6348 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 17:02:02 +0100 Subject: [PATCH 22/76] cancelling stop loss order before selling --- freqtrade/freqtradebot.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f3537f2ab..744f92156 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -505,6 +505,7 @@ class FreqtradeBot(object): 'stake_currency': stake_currency, 'fiat_currency': fiat_currency }) + # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') trade = Trade( @@ -522,6 +523,7 @@ class FreqtradeBot(object): strategy=self.strategy.get_strategy_name(), ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']] ) + Trade.session.add(trade) Trade.session.flush() @@ -798,6 +800,11 @@ class FreqtradeBot(object): sell_type = 'sell' if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): sell_type = 'stoploss' + + # First cancelling stoploss on exchange ... + if self.strategy.stoploss_on_exchange and trade.stoploss_order_id: + self.exchange.cancel_order(trade.stoploss_order_id, trade.pair) + # Execute sell and update trade record order_id = self.exchange.sell(pair=str(trade.pair), ordertype=self.strategy.order_types[sell_type], From bbe8e4e49456b8ec5de23ed22901dfb1c4c82561 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 17:07:37 +0100 Subject: [PATCH 23/76] flake8 --- freqtrade/exchange/__init__.py | 3 ++- freqtrade/freqtradebot.py | 4 ++-- freqtrade/strategy/interface.py | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 59a5da23e..53ae6c2d7 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -349,7 +349,8 @@ class Exchange(object): raise OperationalException( 'In stoploss limit order, stop price should be more than limit price') - return self._api.create_order(pair, 'stop_loss', 'sell', amount, rate, {'stopPrice': stop_price}) + return self._api.create_order(pair, 'stop_loss', 'sell', + amount, rate, {'stopPrice': stop_price}) @retrier def get_balance(self, currency: str) -> float: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 744f92156..40734f385 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -492,8 +492,8 @@ class FreqtradeBot(object): # 0.98 is arbitrary here. limit_price = stop_price * 0.98 - stoploss_order_id = self.exchange.stoploss_limit(pair=pair, amount=amount, - stop_price=stop_price, rate=limit_price)['id'] + stoploss_order_id = self.exchange.stoploss_limit( + pair=pair, amount=amount, stop_price=stop_price, rate=limit_price)['id'] self.rpc.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index df0e3cf72..9047d8807 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -231,8 +231,8 @@ class IStrategy(ABC): stoplossflag = False else: stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, - current_time=date, current_profit=current_profit, - force_stoploss=force_stoploss) + current_time=date, current_profit=current_profit, + force_stoploss=force_stoploss) if stoplossflag.sell_flag: return stoplossflag From 3a1c378325dc97b48bb1ad767fc5ff8281bdf88c Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 17:14:22 +0100 Subject: [PATCH 24/76] typing bugs --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/strategy/interface.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 40734f385..ac9fd758c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -479,13 +479,13 @@ class FreqtradeBot(object): order_id = self.exchange.buy(pair=pair, ordertype=self.strategy.order_types['buy'], amount=amount, rate=buy_limit)['id'] - stoploss_order_id: int = None + stoploss_order_id = None # Check if stoploss should be added on exchange # If True then here immediately after buy we should # Add the stoploss order if self.strategy.stoploss_on_exchange: - stoploss = self.edge.stoploss if self.edge else self.strategy.stoploss + stoploss = self.edge.stoploss(pair=pair) if self.edge else self.strategy.stoploss stop_price = buy_limit * (1 + stoploss) # limit price should be less than stop price. diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 9047d8807..30fc62f42 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -228,7 +228,7 @@ class IStrategy(ABC): current_profit = trade.calc_profit_percent(current_rate) if self.stoploss_on_exchange: - stoplossflag = False + stoplossflag = SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) else: stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, From 2461d86c8d2d0dd692edfabf4fd79162eb87fbd4 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 17:24:45 +0100 Subject: [PATCH 25/76] dry run should consider stop loss is hit on limit price --- freqtrade/freqtradebot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ac9fd758c..281b22bc5 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -805,6 +805,11 @@ class FreqtradeBot(object): if self.strategy.stoploss_on_exchange and trade.stoploss_order_id: self.exchange.cancel_order(trade.stoploss_order_id, trade.pair) + # Dry-run should consider stoploss is executed at the limit price + # So overriding limit in case of dry-run + if self.config['dry_run']: + limit = trade.stop_loss + # Execute sell and update trade record order_id = self.exchange.sell(pair=str(trade.pair), ordertype=self.strategy.order_types[sell_type], From 24df093a85ea65293b6baa653ab515049e4e0e56 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 17:41:01 +0100 Subject: [PATCH 26/76] test: only implemented for binance --- freqtrade/tests/exchange/test_exchange.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 207f14efe..5cbe5b42e 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1171,3 +1171,10 @@ def test_get_fee(default_conf, mocker): ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'get_fee', 'calculate_fee') + +def test_stoploss_limit_available_only_for_binance(default_conf, mocker): + api_mock = MagicMock() + exchange = get_patched_exchange(mocker, default_conf, api_mock) + with pytest.raises(NotImplementedError): + exchange.stoploss_limit('BTC/ETH', 1, 0.8, 0.79) + From 3418592908a8e01699e485993c1d31ffb0d4c291 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 19:25:26 +0100 Subject: [PATCH 27/76] freqtradebot test added for orders on exchange --- freqtrade/exchange/__init__.py | 2 +- freqtrade/tests/conftest.py | 7 ++-- freqtrade/tests/test_freqtradebot.py | 50 ++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 53ae6c2d7..90660c9aa 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -335,7 +335,7 @@ class Exchange(object): def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict: # Only binance is supported - if not self._api.name == 'Binance': + if not self.name == 'Binance': raise NotImplementedError( 'Stoploss limit orders are implemented only for binance as of now.') diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index b6c022b45..3453b4ddf 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -27,12 +27,13 @@ def log_has(line, logs): False) -def patch_exchange(mocker, api_mock=None) -> None: +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_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value="Bittrex")) - mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value="bittrex")) + mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) + mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) + if api_mock: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) else: diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index cef89c250..af4071591 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -880,6 +880,56 @@ def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> Non assert call_args['rate'] == fix_price assert call_args['amount'] == stake_amount / fix_price +def test_execute_buy_with_stoploss_on_exchange(mocker, default_conf, fee, markets, limit_buy_order) -> None: + default_conf['exchange']['name'] = 'binance' + patch_RPCManager(mocker) + patch_exchange(mocker) + freqtrade = FreqtradeBot(default_conf) + freqtrade.strategy.stoploss_on_exchange = True + freqtrade.strategy.stoploss = -0.05 + stake_amount = 2 + bid = 0.11 + get_bid = MagicMock(return_value=bid) + mocker.patch.multiple( + 'freqtrade.freqtradebot.FreqtradeBot', + get_target_bid=get_bid, + _get_min_pair_stake_amount=MagicMock(return_value=1) + ) + buy_mm = MagicMock(return_value={'id': limit_buy_order['id']}) + stoploss_limit = MagicMock(return_value={'id': 13434334}) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 + }), + buy=buy_mm, + get_fee=fee, + get_markets=markets, + stoploss_limit=stoploss_limit + ) + pair = 'ETH/BTC' + print(buy_mm.call_args_list) + + assert freqtrade.execute_buy(pair, stake_amount) + assert stoploss_limit.call_count == 1 + assert get_bid.call_count == 1 + assert buy_mm.call_count == 1 + call_args = buy_mm.call_args_list[0][1] + assert call_args['pair'] == pair + assert call_args['rate'] == bid + assert call_args['amount'] == stake_amount / bid + + call_args = stoploss_limit.call_args_list[0][1] + assert call_args['pair'] == pair + assert call_args['amount'] == stake_amount / bid + assert call_args['stop_price'] == 0.11 * 0.95 + assert call_args['rate'] == 0.11 * 0.95 * 0.98 + + trade = Trade.query.first() + assert trade.is_open is True + assert trade.stoploss_order_id == 13434334 def test_process_maybe_execute_buy(mocker, default_conf) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) From cc1422d448f898f5570224dda448c3995dc9d600 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 19:27:32 +0100 Subject: [PATCH 28/76] flake8 --- freqtrade/tests/test_freqtradebot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index af4071591..03bc68025 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -880,7 +880,9 @@ def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> Non assert call_args['rate'] == fix_price assert call_args['amount'] == stake_amount / fix_price -def test_execute_buy_with_stoploss_on_exchange(mocker, default_conf, fee, markets, limit_buy_order) -> None: + +def test_execute_buy_with_stoploss_on_exchange(mocker, default_conf, + fee, markets, limit_buy_order) -> None: default_conf['exchange']['name'] = 'binance' patch_RPCManager(mocker) patch_exchange(mocker) @@ -931,6 +933,7 @@ def test_execute_buy_with_stoploss_on_exchange(mocker, default_conf, fee, market assert trade.is_open is True assert trade.stoploss_order_id == 13434334 + def test_process_maybe_execute_buy(mocker, default_conf) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) From ecb2c4dca384d08cc9ebf6a112ebb98cefe0b795 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 19:38:20 +0100 Subject: [PATCH 29/76] bloody flake8 --- freqtrade/tests/exchange/test_exchange.py | 2 +- freqtrade/tests/test_freqtradebot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 5cbe5b42e..9dbc50a66 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1172,9 +1172,9 @@ def test_get_fee(default_conf, mocker): ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'get_fee', 'calculate_fee') + def test_stoploss_limit_available_only_for_binance(default_conf, mocker): api_mock = MagicMock() exchange = get_patched_exchange(mocker, default_conf, api_mock) with pytest.raises(NotImplementedError): exchange.stoploss_limit('BTC/ETH', 1, 0.8, 0.79) - diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 03bc68025..571c89bc0 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -885,7 +885,7 @@ def test_execute_buy_with_stoploss_on_exchange(mocker, default_conf, fee, markets, limit_buy_order) -> None: default_conf['exchange']['name'] = 'binance' patch_RPCManager(mocker) - patch_exchange(mocker) + patch_exchange(mocker, id='binance') freqtrade = FreqtradeBot(default_conf) freqtrade.strategy.stoploss_on_exchange = True freqtrade.strategy.stoploss = -0.05 From 07ac9024512e7f96185ee447243ee8d6ccd188f4 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 20:30:31 +0100 Subject: [PATCH 30/76] test exchange added --- freqtrade/exchange/__init__.py | 2 +- freqtrade/tests/conftest.py | 4 +-- freqtrade/tests/exchange/test_exchange.py | 36 +++++++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 90660c9aa..a88fb6ee8 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -345,7 +345,7 @@ class Exchange(object): stop_price = self.symbol_price_prec(pair, stop_price) # Ensure rate is less than stop price - if stop_price >= rate: + if stop_price <= rate: raise OperationalException( 'In stoploss limit order, stop price should be more than limit price') diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 3453b4ddf..f4c263959 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -40,8 +40,8 @@ def patch_exchange(mocker, api_mock=None, id='bittrex') -> None: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock()) -def get_patched_exchange(mocker, config, api_mock=None) -> Exchange: - patch_exchange(mocker, api_mock) +def get_patched_exchange(mocker, config, api_mock=None, id='bittrex') -> Exchange: + patch_exchange(mocker, api_mock, id) exchange = Exchange(config) return exchange diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 9dbc50a66..53e77d7b6 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1178,3 +1178,39 @@ def test_stoploss_limit_available_only_for_binance(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock) with pytest.raises(NotImplementedError): exchange.stoploss_limit('BTC/ETH', 1, 0.8, 0.79) + + +def test_stoploss_limit_order(default_conf, mocker): + api_mock = MagicMock() + order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_type = 'stop_loss' + + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + + 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) + + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') + + with pytest.raises(OperationalException): + order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200) + + api_mock.create_order.reset_mock() + + order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' + assert api_mock.create_order.call_args[0][1] == order_type + assert api_mock.create_order.call_args[0][2] == 'sell' + assert api_mock.create_order.call_args[0][3] == 1 + assert api_mock.create_order.call_args[0][4] == 200 + assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220} From 7faafea8a2949a94725b883e89d81a7e854ec7f5 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 21:01:39 +0100 Subject: [PATCH 31/76] added test for cancelling stop loss before sell --- freqtrade/tests/test_freqtradebot.py | 50 ++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 571c89bc0..333b4d51f 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1528,6 +1528,56 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, } == last_msg +def test_execute_sell_with_stoploss_on_exchange(default_conf, + ticker, fee, ticker_sell_up, + markets, mocker) -> None: + + default_conf['exchange']['name'] = 'binance' + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + get_ticker=ticker, + get_fee=fee, + get_markets=markets + ) + + stoploss_limit = MagicMock(return_value={ + 'id': 123, + 'info': { + 'foo': 'bar' + } + }) + + cancel_order = MagicMock(return_value=True) + + 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) + mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) + mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order) + + freqtrade = FreqtradeBot(default_conf) + freqtrade.strategy.stoploss_on_exchange = True + patch_get_signal(freqtrade) + + # Create some test data + freqtrade.create_trade() + + trade = Trade.query.first() + assert trade + + # Increase the price and sell it + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker_sell_up + ) + + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + + trade = Trade.query.first() + assert trade + assert cancel_order.call_count == 1 + + def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, mocker) -> None: rpc_mock = patch_RPCManager(mocker) From 6f0025c6de194c9f3bf3dd3ea43c135c63abef04 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 21:07:33 +0100 Subject: [PATCH 32/76] documentation written --- docs/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration.md b/docs/configuration.md index 62559a41e..64f0a2ea6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -26,6 +26,7 @@ The table below will list all configuration parameters. | `process_only_new_candles` | false | No | 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. Can be set either in Configuration or in the strategy. | `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file. | `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file. +| `stoploss_on_exchange` | false | No | Only for binance users for now: If this parameter is on then stoploss limit order is executed immediately after buy order is done on binance. | `trailing_stop` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). | `trailing_stop_positve` | 0 | No | Changes stop-loss once profit has been reached. | `trailing_stop_positve_offset` | 0 | No | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. From 1dde56790c4650fa7be00108eb807e9c0e806645 Mon Sep 17 00:00:00 2001 From: misagh Date: Thu, 22 Nov 2018 21:12:49 +0100 Subject: [PATCH 33/76] final broken test fixed --- freqtrade/tests/test_freqtradebot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 333b4d51f..aad5c371f 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1533,6 +1533,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, markets, mocker) -> None: default_conf['exchange']['name'] = 'binance' + rpc_mock = patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', _load_markets=MagicMock(return_value={}), @@ -1576,6 +1577,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, trade = Trade.query.first() assert trade assert cancel_order.call_count == 1 + assert rpc_mock.call_count == 2 def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, From fea77824d09a0ebc9c3f1acf09273b799aa39e23 Mon Sep 17 00:00:00 2001 From: misagh Date: Fri, 23 Nov 2018 15:17:36 +0100 Subject: [PATCH 34/76] handle stop loss on exchange added --- freqtrade/freqtradebot.py | 24 +++++++++ freqtrade/persistence.py | 5 +- freqtrade/strategy/interface.py | 1 + freqtrade/tests/test_freqtradebot.py | 75 +++++++++++++++++++++++++++- 4 files changed, 102 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 281b22bc5..1e5dfd175 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -572,6 +572,17 @@ class FreqtradeBot(object): trade.update(order) + # Check if stoploss on exchnage is hit first + if self.strategy.stoploss_on_exchange and trade.stoploss_order_id: + # Check if stoploss is hit + result = self.handle_stoploss_on_exchage(trade) + + # Updating wallets if stoploss is hit + if result: + self.wallets.update() + + return result + if trade.is_open and trade.open_order_id is None: # Check if we can sell our current pair result = self.handle_trade(trade) @@ -676,6 +687,19 @@ class FreqtradeBot(object): logger.info('Found no sell signals for whitelisted currencies. Trying again..') return False + def handle_stoploss_on_exchage(self, trade: Trade) -> bool: + if not trade.is_open: + raise ValueError(f'attempt to handle stoploss on exchnage for a closed trade: {trade}') + + 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_EXCHNAGE.value + trade.update(order) + return True + else: + return False + def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: if self.edge: stoploss = self.edge.stoploss(trade.pair) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index db6d526c7..02caeeccd 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -178,7 +178,7 @@ class Trade(_DECL_BASE): # absolute value of the initial stop loss initial_stop_loss = Column(Float, nullable=True, default=0.0) # absolute value of the highest reached price - stoploss_order_id = Column(Integer, nullable=True, index=True) + stoploss_order_id = Column(String, nullable=True, index=True) max_rate = Column(Float, nullable=True, default=0.0) sell_reason = Column(String, nullable=True) strategy = Column(String, nullable=True) @@ -250,6 +250,9 @@ class Trade(_DECL_BASE): self.open_order_id = None elif order_type == 'limit' and order['side'] == 'sell': self.close(order['price']) + elif order_type == 'stop_loss_limit': + self.stoploss_order_id = None + self.close(order['price']) else: raise ValueError(f'Unknown order type: {order_type}') cleanup() diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 30fc62f42..d1e22850c 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -33,6 +33,7 @@ class SellType(Enum): """ ROI = "roi" STOP_LOSS = "stop_loss" + STOPLOSS_ON_EXCHNAGE = "stoploss_on_exchange" TRAILING_STOP_LOSS = "trailing_stop_loss" SELL_SIGNAL = "sell_signal" FORCE_SELL = "force_sell" diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index aad5c371f..48918645d 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -931,7 +931,7 @@ def test_execute_buy_with_stoploss_on_exchange(mocker, default_conf, trade = Trade.query.first() assert trade.is_open is True - assert trade.stoploss_order_id == 13434334 + assert trade.stoploss_order_id == '13434334' def test_process_maybe_execute_buy(mocker, default_conf) -> None: @@ -1572,7 +1572,8 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, get_ticker=ticker_sell_up ) - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], + sell_reason=SellType.SELL_SIGNAL) trade = Trade.query.first() assert trade @@ -1580,6 +1581,76 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, assert rpc_mock.call_count == 2 +def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, + ticker, fee, + limit_buy_order, + markets, mocker) -> None: + default_conf['exchange']['name'] = 'binance' + rpc_mock = patch_RPCManager(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + get_ticker=ticker, + get_fee=fee, + get_markets=markets + ) + + stoploss_limit = MagicMock(return_value={ + 'id': 123, + 'info': { + 'foo': 'bar' + } + }) + + 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) + mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) + + freqtrade = FreqtradeBot(default_conf) + freqtrade.strategy.stoploss_on_exchange = True + patch_get_signal(freqtrade) + + # Create some test data + freqtrade.create_trade() + + trade = Trade.query.first() + assert trade + assert trade.stoploss_order_id == '123' + assert trade.open_order_id is not None + + trade.update(limit_buy_order) + + # Assuming stoploss on exchnage is hit + # stoploss_order_id should become None + # and trade should be sold at the price of stoploss + stoploss_limit_executed = MagicMock(return_value={ + "id": "123", + "timestamp": 1542707426845, + "datetime": "2018-11-20T09:50:26.845Z", + "lastTradeTimestamp": None, + "symbol": "BTC/USDT", + "type": "stop_loss_limit", + "side": "sell", + "price": 1.08801, + "amount": 90.99181074, + "cost": 99.0000000032274, + "average": 1.08801, + "filled": 90.99181074, + "remaining": 0.0, + "status": "closed", + "fee": None, + "trades": None + }) + mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_limit_executed) + + freqtrade.process_maybe_execute_sell(trade) + assert trade.stoploss_order_id is None + assert trade.is_open is False + print(trade.sell_reason) + assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHNAGE.value + assert rpc_mock.call_count == 1 + + def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, mocker) -> None: rpc_mock = patch_RPCManager(mocker) From 5ee2faa182fd8c5fd470e50febf266648fbbaf21 Mon Sep 17 00:00:00 2001 From: misagh Date: Fri, 23 Nov 2018 19:17:36 +0100 Subject: [PATCH 35/76] adding stop loss on exchange after the buy order is fulfilled not before. --- freqtrade/freqtradebot.py | 41 +++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1e5dfd175..c2ef0e406 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -479,22 +479,6 @@ class FreqtradeBot(object): order_id = self.exchange.buy(pair=pair, ordertype=self.strategy.order_types['buy'], amount=amount, rate=buy_limit)['id'] - stoploss_order_id = None - - # Check if stoploss should be added on exchange - # If True then here immediately after buy we should - # Add the stoploss order - if self.strategy.stoploss_on_exchange: - stoploss = self.edge.stoploss(pair=pair) if self.edge else self.strategy.stoploss - stop_price = buy_limit * (1 + stoploss) - - # limit price should be less than stop price. - # 0.98 is arbitrary here. - limit_price = stop_price * 0.98 - - stoploss_order_id = self.exchange.stoploss_limit( - pair=pair, amount=amount, stop_price=stop_price, rate=limit_price)['id'] - self.rpc.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': self.exchange.name.capitalize(), @@ -519,7 +503,6 @@ class FreqtradeBot(object): open_date=datetime.utcnow(), exchange=self.exchange.id, open_order_id=order_id, - stoploss_order_id=stoploss_order_id, strategy=self.strategy.get_strategy_name(), ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']] ) @@ -572,16 +555,32 @@ class FreqtradeBot(object): trade.update(order) - # Check if stoploss on exchnage is hit first - if self.strategy.stoploss_on_exchange and trade.stoploss_order_id: + # Check uf trade is fulfulled in which case the stoploss + # on exchange should be added immediately if stoploss on exchnage + # is on + if self.strategy.stoploss_on_exchange and trade.is_open and \ + trade.open_order_id is None and trade.stoploss_order_id is None: + + stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss + stop_price = trade.open_rate * (1 + stoploss) + + # limit price should be less than stop price. + # 0.98 is arbitrary here. + limit_price = stop_price * 0.98 + + 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 = stoploss_order_id + + # Or Check if there is a stoploss on exchnage and it is hit + elif self.strategy.stoploss_on_exchange and trade.stoploss_order_id: # Check if stoploss is hit result = self.handle_stoploss_on_exchage(trade) # Updating wallets if stoploss is hit if result: self.wallets.update() - - return result + return result if trade.is_open and trade.open_order_id is None: # Check if we can sell our current pair From 9144a8f79df3c3839e294e4d2285a2a123aa3993 Mon Sep 17 00:00:00 2001 From: misagh Date: Fri, 23 Nov 2018 20:28:01 +0100 Subject: [PATCH 36/76] tests fixed --- freqtrade/freqtradebot.py | 14 ++++-- freqtrade/tests/test_freqtradebot.py | 70 +++++++++------------------- 2 files changed, 31 insertions(+), 53 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c2ef0e406..d4ea83d25 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -559,9 +559,13 @@ class FreqtradeBot(object): # on exchange should be added immediately if stoploss on exchnage # is on if self.strategy.stoploss_on_exchange and trade.is_open and \ - trade.open_order_id is None and trade.stoploss_order_id is None: + trade.open_order_id is None and trade.stoploss_order_id is None: + + if self.edge: + stoploss = self.edge.stoploss(pair=trade.pair) + else: + stoploss = self.strategy.stoploss - stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss stop_price = trade.open_rate * (1 + stoploss) # limit price should be less than stop price. @@ -569,8 +573,10 @@ class FreqtradeBot(object): limit_price = stop_price * 0.98 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 = stoploss_order_id + pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price + )['id'] + + trade.stoploss_order_id = str(stoploss_order_id) # Or Check if there is a stoploss on exchnage and it is hit elif self.strategy.stoploss_on_exchange and trade.stoploss_order_id: diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 48918645d..64c8e9765 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -881,57 +881,29 @@ def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> Non assert call_args['amount'] == stake_amount / fix_price -def test_execute_buy_with_stoploss_on_exchange(mocker, default_conf, - fee, markets, limit_buy_order) -> None: - default_conf['exchange']['name'] = 'binance' +def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None: patch_RPCManager(mocker) - patch_exchange(mocker, id='binance') + 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']) + + stoploss_limit = MagicMock(return_value={'id': 13434334}) + mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) + freqtrade = FreqtradeBot(default_conf) freqtrade.strategy.stoploss_on_exchange = True - freqtrade.strategy.stoploss = -0.05 - stake_amount = 2 - bid = 0.11 - get_bid = MagicMock(return_value=bid) - mocker.patch.multiple( - 'freqtrade.freqtradebot.FreqtradeBot', - get_target_bid=get_bid, - _get_min_pair_stake_amount=MagicMock(return_value=1) - ) - buy_mm = MagicMock(return_value={'id': limit_buy_order['id']}) - stoploss_limit = MagicMock(return_value={'id': 13434334}) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ - 'bid': 0.00001172, - 'ask': 0.00001173, - 'last': 0.00001172 - }), - buy=buy_mm, - get_fee=fee, - get_markets=markets, - stoploss_limit=stoploss_limit - ) - pair = 'ETH/BTC' - print(buy_mm.call_args_list) - assert freqtrade.execute_buy(pair, stake_amount) - assert stoploss_limit.call_count == 1 - assert get_bid.call_count == 1 - assert buy_mm.call_count == 1 - call_args = buy_mm.call_args_list[0][1] - assert call_args['pair'] == pair - assert call_args['rate'] == bid - assert call_args['amount'] == stake_amount / bid + trade = MagicMock() + trade.open_order_id = None + trade.stoploss_order_id = None + trade.is_open = True - call_args = stoploss_limit.call_args_list[0][1] - assert call_args['pair'] == pair - assert call_args['amount'] == stake_amount / bid - assert call_args['stop_price'] == 0.11 * 0.95 - assert call_args['rate'] == 0.11 * 0.95 * 0.98 - - trade = Trade.query.first() - assert trade.is_open is True + freqtrade.process_maybe_execute_sell(trade) assert trade.stoploss_order_id == '13434334' + assert stoploss_limit.call_count == 1 + assert trade.is_open is True def test_process_maybe_execute_buy(mocker, default_conf) -> None: @@ -1566,6 +1538,8 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, trade = Trade.query.first() assert trade + freqtrade.process_maybe_execute_sell(trade) + # Increase the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1612,13 +1586,11 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, # Create some test data freqtrade.create_trade() - trade = Trade.query.first() + freqtrade.process_maybe_execute_sell(trade) assert trade assert trade.stoploss_order_id == '123' - assert trade.open_order_id is not None - - trade.update(limit_buy_order) + assert trade.open_order_id is None # Assuming stoploss on exchnage is hit # stoploss_order_id should become None From 1c2c19b12cfa60951e2169c77277452e412374b0 Mon Sep 17 00:00:00 2001 From: misagh Date: Fri, 23 Nov 2018 20:47:17 +0100 Subject: [PATCH 37/76] the complex in the life of flake8 resolved --- freqtrade/freqtradebot.py | 69 ++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d4ea83d25..3d52ffffa 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -555,35 +555,8 @@ class FreqtradeBot(object): trade.update(order) - # Check uf trade is fulfulled in which case the stoploss - # on exchange should be added immediately if stoploss on exchnage - # is on - if self.strategy.stoploss_on_exchange and trade.is_open and \ - trade.open_order_id is None and trade.stoploss_order_id is None: - - 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. - # 0.98 is arbitrary here. - limit_price = stop_price * 0.98 - - 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) - - # Or Check if there is a stoploss on exchnage and it is hit - elif self.strategy.stoploss_on_exchange and trade.stoploss_order_id: - # Check if stoploss is hit + if self.strategy.stoploss_on_exchange: result = self.handle_stoploss_on_exchage(trade) - - # Updating wallets if stoploss is hit if result: self.wallets.update() return result @@ -693,18 +666,40 @@ class FreqtradeBot(object): return False def handle_stoploss_on_exchage(self, trade: Trade) -> bool: - if not trade.is_open: - raise ValueError(f'attempt to handle stoploss on exchnage for a closed trade: {trade}') + # Check uf trade is fulfulled in which case the stoploss + # on exchange should be added immediately if stoploss on exchnage + # is on + if trade.is_open and trade.open_order_id is None and trade.stoploss_order_id is None: + if self.edge: + stoploss = self.edge.stoploss(pair=trade.pair) + else: + stoploss = self.strategy.stoploss - 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_EXCHNAGE.value - trade.update(order) - return True - else: + stop_price = trade.open_rate * (1 + stoploss) + + # limit price should be less than stop price. + # 0.98 is arbitrary here. + limit_price = stop_price * 0.98 + + 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) return False + # Or Check if there is a stoploss on exchnage and 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_EXCHNAGE.value + trade.update(order) + return True + else: + return False + + def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: if self.edge: stoploss = self.edge.stoploss(trade.pair) From 89eb3d9f36d960d8862bf4fb66b02f2e12a4d7c4 Mon Sep 17 00:00:00 2001 From: misagh Date: Fri, 23 Nov 2018 20:49:00 +0100 Subject: [PATCH 38/76] blank line removed --- freqtrade/freqtradebot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3d52ffffa..bd4e1b9e7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -699,7 +699,6 @@ class FreqtradeBot(object): else: return False - def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: if self.edge: stoploss = self.edge.stoploss(trade.pair) From dedf1ff70340df99647d0f30cc556dcaedc3c291 Mon Sep 17 00:00:00 2001 From: misagh Date: Fri, 23 Nov 2018 20:51:23 +0100 Subject: [PATCH 39/76] refactoring --- freqtrade/freqtradebot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index bd4e1b9e7..335a0f76e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -669,6 +669,7 @@ class FreqtradeBot(object): # Check uf trade is fulfulled in which case the stoploss # on exchange should be added immediately if stoploss on exchnage # is on + result = False if trade.is_open and trade.open_order_id is None and trade.stoploss_order_id is None: if self.edge: stoploss = self.edge.stoploss(pair=trade.pair) @@ -684,9 +685,7 @@ class FreqtradeBot(object): 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) - return False # Or Check if there is a stoploss on exchnage and it is hit elif trade.stoploss_order_id: @@ -695,9 +694,10 @@ class FreqtradeBot(object): if order['status'] == 'closed': trade.sell_reason = SellType.STOPLOSS_ON_EXCHNAGE.value trade.update(order) - return True + result = True else: - return False + result = False + return result def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: if self.edge: From 1a8e9ebc0f8e60239015fc17dbde7ed7bcd0f77d Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 16:53:10 +0100 Subject: [PATCH 40/76] stoploss_order_id added to migration script --- freqtrade/persistence.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 02caeeccd..364af06ce 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -91,6 +91,7 @@ def check_migrate(engine) -> None: close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null') stop_loss = get_column_def(cols, 'stop_loss', '0.0') initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0') + stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null') max_rate = get_column_def(cols, 'max_rate', '0.0') sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') @@ -106,7 +107,7 @@ 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, max_rate, sell_reason, strategy, + stop_loss, initial_stop_loss, stoploss_order_id, max_rate, sell_reason, strategy, ticker_interval ) select id, lower(exchange), @@ -122,7 +123,8 @@ def check_migrate(engine) -> None: {close_rate_requested} close_rate_requested, close_profit, stake_amount, amount, open_date, close_date, open_order_id, {stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss, - {max_rate} max_rate, {sell_reason} sell_reason, {strategy} strategy, + {stoploss_order_id} stoploss_order_id, {max_rate} max_rate, + {sell_reason} sell_reason, {strategy} strategy, {ticker_interval} ticker_interval from {table_back_name} """) @@ -177,8 +179,9 @@ class Trade(_DECL_BASE): stop_loss = Column(Float, nullable=True, default=0.0) # absolute value of the initial stop loss initial_stop_loss = Column(Float, nullable=True, default=0.0) - # absolute value of the highest reached price + # stoploss order id which is on exchange stoploss_order_id = Column(String, nullable=True, index=True) + # absolute value of the highest reached price max_rate = Column(Float, nullable=True, default=0.0) sell_reason = Column(String, nullable=True) strategy = Column(String, nullable=True) From a9ec5c66993a00bb8e7d6ec57b3a65079d419144 Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 17:07:35 +0100 Subject: [PATCH 41/76] simplifying if conditions --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 335a0f76e..f197d9117 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -670,7 +670,7 @@ class FreqtradeBot(object): # on exchange should be added immediately if stoploss on exchnage # is on result = False - if trade.is_open and trade.open_order_id is None and trade.stoploss_order_id is None: + if trade.is_open and not trade.open_order_id and not trade.stoploss_order_id: if self.edge: stoploss = self.edge.stoploss(pair=trade.pair) else: From afd0a054b2a6bfdac724d61f6e4a23c80cd0bc71 Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 17:08:12 +0100 Subject: [PATCH 42/76] typo corrected --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f197d9117..a8736d6f1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -556,7 +556,7 @@ class FreqtradeBot(object): trade.update(order) if self.strategy.stoploss_on_exchange: - result = self.handle_stoploss_on_exchage(trade) + result = self.handle_stoploss_on_exchange(trade) if result: self.wallets.update() return result @@ -665,7 +665,7 @@ class FreqtradeBot(object): logger.info('Found no sell signals for whitelisted currencies. Trying again..') return False - def handle_stoploss_on_exchage(self, trade: Trade) -> bool: + def handle_stoploss_on_exchange(self, trade: Trade) -> bool: # Check uf trade is fulfulled in which case the stoploss # on exchange should be added immediately if stoploss on exchnage # is on From 531d9ecd0c8f15f9bf7721d6d0b691486a240740 Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 17:10:51 +0100 Subject: [PATCH 43/76] docstring added --- freqtrade/freqtradebot.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a8736d6f1..369bed173 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -666,9 +666,12 @@ class FreqtradeBot(object): return False def handle_stoploss_on_exchange(self, trade: Trade) -> bool: - # Check uf trade is fulfulled in which case the stoploss - # on exchange should be added immediately if stoploss on exchnage - # is on + """ + Check if trade is fulfilled in which case the stoploss + on exchange should be added immediately if stoploss on exchnage + is enabled. + """ + result = False if trade.is_open and not trade.open_order_id and not trade.stoploss_order_id: if self.edge: From 870631f324eb785fb0faf12c4544b742dc59cb09 Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 17:32:25 +0100 Subject: [PATCH 44/76] 1) comments added to handle_sl 2) dry-run force price removed --- freqtrade/freqtradebot.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 369bed173..cf22ee52a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -673,6 +673,9 @@ class FreqtradeBot(object): """ result = False + + # If trade is open and the buy order is fulfilled but there is no stoploss, + # then we add a stoploss on exchange if trade.is_open and not trade.open_order_id and not trade.stoploss_order_id: if self.edge: stoploss = self.edge.stoploss(pair=trade.pair) @@ -690,7 +693,8 @@ class FreqtradeBot(object): )['id'] trade.stoploss_order_id = str(stoploss_order_id) - # Or Check if there is a stoploss on exchnage and it is hit + # Or there is already a stoploss on exchnage. + # 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) @@ -831,11 +835,6 @@ class FreqtradeBot(object): if self.strategy.stoploss_on_exchange and trade.stoploss_order_id: self.exchange.cancel_order(trade.stoploss_order_id, trade.pair) - # Dry-run should consider stoploss is executed at the limit price - # So overriding limit in case of dry-run - if self.config['dry_run']: - limit = trade.stop_loss - # Execute sell and update trade record order_id = self.exchange.sell(pair=str(trade.pair), ordertype=self.strategy.order_types[sell_type], From 000711b0256e541bf8dada406ae6ad8d77c24701 Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 18:08:11 +0100 Subject: [PATCH 45/76] added stoploss_limit_order for dry-run --- freqtrade/tests/exchange/test_exchange.py | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 53e77d7b6..402596da8 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1214,3 +1214,39 @@ def test_stoploss_limit_order(default_conf, mocker): assert api_mock.create_order.call_args[0][3] == 1 assert api_mock.create_order.call_args[0][4] == 200 assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220} + + +def test_stoploss_limit_order_dry_run(default_conf, mocker): + api_mock = MagicMock() + order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_type = 'stop_loss' + + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + + default_conf['dry_run'] = True + 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) + + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') + + with pytest.raises(OperationalException): + order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200) + + api_mock.create_order.reset_mock() + + order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' + assert api_mock.create_order.call_args[0][1] == order_type + assert api_mock.create_order.call_args[0][2] == 'sell' + assert api_mock.create_order.call_args[0][3] == 1 + assert api_mock.create_order.call_args[0][4] == 200 + assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220} From b2c0b20a58c161961d13bb1fa34494516f93332e Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 18:26:04 +0100 Subject: [PATCH 46/76] added real tests for stop on exchange in dry-run --- freqtrade/exchange/__init__.py | 17 +++++++++++++++++ freqtrade/tests/exchange/test_exchange.py | 23 ++++++----------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index a88fb6ee8..fc28516f4 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -349,6 +349,23 @@ class Exchange(object): raise OperationalException( 'In stoploss limit order, stop price should be more than limit price') + if self._conf['dry_run']: + order_id = f'dry_run_buy_{randint(0, 10**6)}' + self._dry_run_open_orders[order_id] = { + 'info': {}, + 'id': order_id, + 'pair': pair, + 'price': stop_price, + 'amount': amount, + 'type': 'stop_loss_limit', + 'side': 'sell', + 'remaining': amount, + 'datetime': arrow.utcnow().isoformat(), + 'status': 'open', + 'fee': None + } + return self._dry_run_open_orders[order_id] + return self._api.create_order(pair, 'stop_loss', 'sell', amount, rate, {'stopPrice': stop_price}) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 402596da8..5ae6a031a 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1218,16 +1218,7 @@ def test_stoploss_limit_order(default_conf, mocker): def test_stoploss_limit_order_dry_run(default_conf, mocker): api_mock = MagicMock() - order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) - order_type = 'stop_loss' - - api_mock.create_order = MagicMock(return_value={ - 'id': order_id, - 'info': { - 'foo': 'bar' - } - }) - + order_type = 'stop_loss_limit' default_conf['dry_run'] = True 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) @@ -1243,10 +1234,8 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker): assert 'id' in order assert 'info' in order - assert order['id'] == order_id - assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' - assert api_mock.create_order.call_args[0][1] == order_type - assert api_mock.create_order.call_args[0][2] == 'sell' - assert api_mock.create_order.call_args[0][3] == 1 - assert api_mock.create_order.call_args[0][4] == 200 - assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220} + assert 'type' in order + + assert order['type'] == order_type + assert order['price'] == 220 + assert order['amount'] == 1 From fe8927136c6427c3351017256489c9d18a2c2094 Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 18:36:07 +0100 Subject: [PATCH 47/76] typo --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index cf22ee52a..126cc485f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -693,7 +693,7 @@ class FreqtradeBot(object): )['id'] trade.stoploss_order_id = str(stoploss_order_id) - # Or there is already a stoploss on exchnage. + # Or 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) From b5192880df34ef09e6eb8b25dce0d51a64a4a777 Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 19:00:59 +0100 Subject: [PATCH 48/76] [WIP] adding tests for handle_stoploss_on_exchange. --- freqtrade/tests/test_freqtradebot.py | 76 ++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 64c8e9765..4ecd287d6 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -906,6 +906,82 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None assert trade.is_open is True +def test_handle_stoploss_on_exchange(mocker, default_conf, fee, + markets, limit_buy_order, limit_sell_order) -> None: + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + stoploss_limit = MagicMock(return_value={'id': 13434334}) + mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) + + # First case: when stoploss is not yet set but the order is open + # should get the stoploss order id immediately + # and should return false as no trade actually happened + trade = MagicMock() + trade.is_open = True + trade.open_order_id = None + trade.stoploss_order_id = None + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert stoploss_limit.call_count == 1 + assert trade.stoploss_order_id == "13434334" + + trade.reset_mock() + + # Second case: when stoploss is set but it is not yet hit + # should do nothing and return false + trade.is_open = True + trade.open_order_id = None + trade.stoploss_order_id = 100 + + hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) + mocker.patch('freqtrade.exchange.Exchange.get_order', hanging_stoploss_order) + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert trade.stoploss_order_id == 100 + + trade.reset_mock() + + # Third case: when stoploss is set and it is hit + # should unset stoploss_order_id and return true + # as a trade actually happened + patch_RPCManager(mocker) + 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, + get_markets=markets + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + freqtrade.create_trade() + + trade = Trade.query.first() + assert trade + + # trade = freqtrade.create + # trade.is_open = True + # trade.open_order_id = None + # trade.stoploss_order_id = 100 + + # stoploss_order_hit = MagicMock(return_value={'status': 'closed'}) + # mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit) + + # # trade = Trade.query.first() + # # assert trade + # assert freqtrade.handle_stoploss_on_exchange(trade) is True + # time.sleep(0.01) # Race condition fix + # assert trade.is_open is True + # assert trade.stoploss_order_id is None + + def test_process_maybe_execute_buy(mocker, default_conf) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) From c8a0956e1bc34fbb418de13d08eac686b3cd6581 Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 19:12:00 +0100 Subject: [PATCH 49/76] fixed test handle_stoploss_on_exchange --- freqtrade/tests/test_freqtradebot.py | 68 ++++++++++++---------------- 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 4ecd287d6..9b1f40ccf 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -908,10 +908,24 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None def test_handle_stoploss_on_exchange(mocker, default_conf, fee, markets, limit_buy_order, limit_sell_order) -> None: - - freqtrade = get_patched_freqtradebot(mocker, default_conf) stoploss_limit = MagicMock(return_value={'id': 13434334}) - mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) + patch_RPCManager(mocker) + 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, + get_markets=markets, + stoploss_limit=stoploss_limit + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) # First case: when stoploss is not yet set but the order is open # should get the stoploss order id immediately @@ -925,8 +939,6 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, assert stoploss_limit.call_count == 1 assert trade.stoploss_order_id == "13434334" - trade.reset_mock() - # Second case: when stoploss is set but it is not yet hit # should do nothing and return false trade.is_open = True @@ -939,47 +951,25 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.stoploss_order_id == 100 - trade.reset_mock() - # Third case: when stoploss is set and it is hit # should unset stoploss_order_id and return true # as a trade actually happened - patch_RPCManager(mocker) - 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, - get_markets=markets - ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - freqtrade.create_trade() - trade = Trade.query.first() + trade.is_open = True + trade.open_order_id = None + trade.stoploss_order_id = 100 assert trade - # trade = freqtrade.create - # trade.is_open = True - # trade.open_order_id = None - # trade.stoploss_order_id = 100 - - # stoploss_order_hit = MagicMock(return_value={'status': 'closed'}) - # mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit) - - # # trade = Trade.query.first() - # # assert trade - # assert freqtrade.handle_stoploss_on_exchange(trade) is True - # time.sleep(0.01) # Race condition fix - # assert trade.is_open is True - # assert trade.stoploss_order_id is None + stoploss_order_hit = MagicMock(return_value={ + 'status': 'closed', + 'type': 'stop_loss_limit', + 'price': 2 + }) + mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit) + assert freqtrade.handle_stoploss_on_exchange(trade) is True + assert trade.stoploss_order_id is None + assert trade.is_open is False def test_process_maybe_execute_buy(mocker, default_conf) -> None: From 519b1f00e2932d034afb76ee999f4273b8c7467a Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 20:12:50 +0100 Subject: [PATCH 50/76] adding strategy config consistency function --- freqtrade/freqtradebot.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 126cc485f..dc1189bb9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -54,11 +54,14 @@ class FreqtradeBot(object): # Init objects self.config = config self.strategy: IStrategy = StrategyResolver(self.config).strategy + self.check_strategy_config_consistency(config, self.strategy) + self.rpc: RPCManager = RPCManager(self) self.persistence = None self.exchange = Exchange(self.config) self.wallets = Wallets(self.exchange) + # Initializing Edge only if enabled self.edge = Edge(self.config, self.exchange, self.strategy) if \ self.config.get('edge', {}).get('enabled', False) else None @@ -66,6 +69,16 @@ class FreqtradeBot(object): self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] self._init_modules() + def check_strategy_config_consistency(self, config, strategy: IStrategy) -> None: + """ + checks if config is compatible with the given strategy + """ + + # Stoploss on exchange is only implemented for binance + if strategy.stoploss_on_exchange and config.get('exchange') is not 'binance': + raise OperationalException( + 'Stoploss limit orders are implemented only for binance as of now.') + def _init_modules(self) -> None: """ Initializes all modules and updates the config From 266bd7b9b62d095d7f5d703e2cd8f27439102abd Mon Sep 17 00:00:00 2001 From: misagh Date: Sat, 24 Nov 2018 21:42:15 +0100 Subject: [PATCH 51/76] error message improved --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index dc1189bb9..fbabb7d90 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -77,7 +77,7 @@ class FreqtradeBot(object): # Stoploss on exchange is only implemented for binance if strategy.stoploss_on_exchange and config.get('exchange') is not 'binance': raise OperationalException( - 'Stoploss limit orders are implemented only for binance as of now.') + 'On exchange stoploss is not supported for %s.' % config.get('exchange')) def _init_modules(self) -> None: """ From 664b96173eabe2e6472e6a812e631aa6378080eb Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 10:54:36 +0100 Subject: [PATCH 52/76] removing NotImplementedError from stoploss_limit --- freqtrade/exchange/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index fc28516f4..3ccd2369a 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -334,10 +334,10 @@ class Exchange(object): raise OperationalException(e) def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict: - # Only binance is supported - if not self.name == 'Binance': - raise NotImplementedError( - 'Stoploss limit orders are implemented only for binance as of now.') + """ + creates a stoploss limit order. + NOTICE: it is not supported by all exchanges. only binance is tested for now. + """ # Set the precision for amount and price(rate) as accepted by the exchange amount = self.symbol_amount_prec(pair, amount) From dcae3a26440a07cadb362999003a2d81c6486157 Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 11:29:04 +0100 Subject: [PATCH 53/76] test of check_consistency added --- freqtrade/freqtradebot.py | 5 ++--- freqtrade/tests/exchange/test_exchange.py | 7 ------- freqtrade/tests/test_freqtradebot.py | 6 ++++++ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fbabb7d90..005f698dd 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -61,7 +61,6 @@ class FreqtradeBot(object): self.exchange = Exchange(self.config) self.wallets = Wallets(self.exchange) - # Initializing Edge only if enabled self.edge = Edge(self.config, self.exchange, self.strategy) if \ self.config.get('edge', {}).get('enabled', False) else None @@ -73,11 +72,11 @@ class FreqtradeBot(object): """ checks if config is compatible with the given strategy """ - # Stoploss on exchange is only implemented for binance if strategy.stoploss_on_exchange and config.get('exchange') is not 'binance': raise OperationalException( - 'On exchange stoploss is not supported for %s.' % config.get('exchange')) + 'On exchange stoploss is not supported for %s.' % config['exchange']['name'] + ) def _init_modules(self) -> None: """ diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 5ae6a031a..57be54262 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1173,13 +1173,6 @@ def test_get_fee(default_conf, mocker): 'get_fee', 'calculate_fee') -def test_stoploss_limit_available_only_for_binance(default_conf, mocker): - api_mock = MagicMock() - exchange = get_patched_exchange(mocker, default_conf, api_mock) - with pytest.raises(NotImplementedError): - exchange.stoploss_limit('BTC/ETH', 1, 0.8, 0.79) - - def test_stoploss_limit_order(default_conf, mocker): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 9b1f40ccf..5acf4fdcb 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2516,3 +2516,9 @@ def test_startup_messages(default_conf, mocker): default_conf['dynamic_whitelist'] = 20 freqtrade = get_patched_freqtradebot(mocker, default_conf) assert freqtrade.state is State.RUNNING + + +def test_check_consistency(default_conf, mocker, caplog): + mocker.patch('freqtrade.freqtradebot.IStrategy.stoploss_on_exchange', True) + with pytest.raises(OperationalException): + FreqtradeBot(default_conf) From e4744c1ba4cb428fc40da67c9b960e57b0c275b8 Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 11:31:30 +0100 Subject: [PATCH 54/76] stop loss on exchanged removed from doc --- docs/configuration.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 64f0a2ea6..62559a41e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -26,7 +26,6 @@ The table below will list all configuration parameters. | `process_only_new_candles` | false | No | 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. Can be set either in Configuration or in the strategy. | `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file. | `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file. -| `stoploss_on_exchange` | false | No | Only for binance users for now: If this parameter is on then stoploss limit order is executed immediately after buy order is done on binance. | `trailing_stop` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). | `trailing_stop_positve` | 0 | No | Changes stop-loss once profit has been reached. | `trailing_stop_positve_offset` | 0 | No | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. From 3e29fbb17a49544c77b06e46776f53dcd6f674bc Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 17:22:56 +0100 Subject: [PATCH 55/76] stoploss on exchange added as a parameter to order_types --- freqtrade/constants.py | 3 ++- freqtrade/exchange/__init__.py | 6 ++++++ freqtrade/freqtradebot.py | 15 ++------------- freqtrade/strategy/default_strategy.py | 3 ++- freqtrade/strategy/interface.py | 10 +++------- freqtrade/tests/exchange/test_exchange.py | 20 +++++++++++++++++++- freqtrade/tests/test_freqtradebot.py | 12 +++--------- 7 files changed, 37 insertions(+), 32 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 055fee3b2..481a219d6 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -109,7 +109,8 @@ CONF_SCHEMA = { 'properties': { 'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, - 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES} + 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, + 'stoploss_on_exchange': {'type': 'boolean'} }, 'required': ['buy', 'sell', 'stoploss'] }, diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 3ccd2369a..6e826794a 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -227,6 +227,12 @@ class Exchange(object): raise OperationalException( f'Exchange {self.name} does not support market orders.') + if order_types.get('stoploss_on_exchange', False): + if self.name is not 'Binance': + raise OperationalException( + 'On exchange stoploss is not supported for %s.' % self.name + ) + def exchange_has(self, endpoint: str) -> bool: """ Checks if exchange implements a specific API endpoint. diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 005f698dd..d9a15f56a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -54,7 +54,6 @@ class FreqtradeBot(object): # Init objects self.config = config self.strategy: IStrategy = StrategyResolver(self.config).strategy - self.check_strategy_config_consistency(config, self.strategy) self.rpc: RPCManager = RPCManager(self) self.persistence = None @@ -68,16 +67,6 @@ class FreqtradeBot(object): self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] self._init_modules() - def check_strategy_config_consistency(self, config, strategy: IStrategy) -> None: - """ - checks if config is compatible with the given strategy - """ - # Stoploss on exchange is only implemented for binance - if strategy.stoploss_on_exchange and config.get('exchange') is not 'binance': - raise OperationalException( - 'On exchange stoploss is not supported for %s.' % config['exchange']['name'] - ) - def _init_modules(self) -> None: """ Initializes all modules and updates the config @@ -567,7 +556,7 @@ class FreqtradeBot(object): trade.update(order) - if self.strategy.stoploss_on_exchange: + if self.strategy.order_types.get('stoploss_on_exchange'): result = self.handle_stoploss_on_exchange(trade) if result: self.wallets.update() @@ -844,7 +833,7 @@ class FreqtradeBot(object): sell_type = 'stoploss' # First cancelling stoploss on exchange ... - if self.strategy.stoploss_on_exchange and trade.stoploss_order_id: + if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: self.exchange.cancel_order(trade.stoploss_order_id, trade.pair) # Execute sell and update trade record diff --git a/freqtrade/strategy/default_strategy.py b/freqtrade/strategy/default_strategy.py index b282a5938..9c850a8be 100644 --- a/freqtrade/strategy/default_strategy.py +++ b/freqtrade/strategy/default_strategy.py @@ -32,7 +32,8 @@ class DefaultStrategy(IStrategy): order_types = { 'buy': 'limit', 'sell': 'limit', - 'stoploss': 'limit' + 'stoploss': 'limit', + 'stoploss_on_exchange': False } def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d1e22850c..1073f8028 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -68,11 +68,6 @@ class IStrategy(ABC): # associated stoploss stoploss: float - # if the stoploss should be on exchange. - # if this is True then a stoploss order will be placed - # immediately after a successful buy order. - stoploss_on_exchange: bool = False - # associated ticker interval ticker_interval: str @@ -80,7 +75,8 @@ class IStrategy(ABC): order_types: Dict = { 'buy': 'limit', 'sell': 'limit', - 'stoploss': 'limit' + 'stoploss': 'limit', + 'stoploss_on_exchange': False } # run "populate_indicators" only for new candle @@ -228,7 +224,7 @@ class IStrategy(ABC): current_rate = low or rate current_profit = trade.calc_profit_percent(current_rate) - if self.stoploss_on_exchange: + if self.order_types.get('stoploss_on_exchange'): stoplossflag = SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) else: stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 57be54262..1a46ff001 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -362,7 +362,14 @@ def test_validate_order_types(default_conf, mocker): 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_timeframes', MagicMock()) - default_conf['order_types'] = {'buy': 'limit', 'sell': 'limit', 'stoploss': 'market'} + mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex') + default_conf['order_types'] = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'market', + 'stoploss_on_exchange': False + } + Exchange(default_conf) type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False}) @@ -374,6 +381,17 @@ def test_validate_order_types(default_conf, mocker): match=r'Exchange .* does not support market orders.'): Exchange(default_conf) + default_conf['order_types'] = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': True + } + + with pytest.raises(OperationalException, + match=r'On exchange stoploss is not supported for .*'): + Exchange(default_conf) + def test_validate_order_types_not_in_config(default_conf, mocker): api_mock = MagicMock() diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 5acf4fdcb..b6b42d1da 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -893,7 +893,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) freqtrade = FreqtradeBot(default_conf) - freqtrade.strategy.stoploss_on_exchange = True + freqtrade.strategy.order_types['stoploss_on_exchange'] = True trade = MagicMock() trade.open_order_id = None @@ -1595,7 +1595,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order) freqtrade = FreqtradeBot(default_conf) - freqtrade.strategy.stoploss_on_exchange = True + freqtrade.strategy.order_types['stoploss_on_exchange'] = True patch_get_signal(freqtrade) # Create some test data @@ -1647,7 +1647,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) freqtrade = FreqtradeBot(default_conf) - freqtrade.strategy.stoploss_on_exchange = True + freqtrade.strategy.order_types['stoploss_on_exchange'] = True patch_get_signal(freqtrade) # Create some test data @@ -2516,9 +2516,3 @@ def test_startup_messages(default_conf, mocker): default_conf['dynamic_whitelist'] = 20 freqtrade = get_patched_freqtradebot(mocker, default_conf) assert freqtrade.state is State.RUNNING - - -def test_check_consistency(default_conf, mocker, caplog): - mocker.patch('freqtrade.freqtradebot.IStrategy.stoploss_on_exchange', True) - with pytest.raises(OperationalException): - FreqtradeBot(default_conf) From 5e1fb11124cf67df36e29876698450714132a51a Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 17:30:06 +0100 Subject: [PATCH 56/76] documentation added for stop loss on exchange --- config_full.json.example | 3 ++- docs/configuration.md | 9 +++++---- freqtrade/constants.py | 2 +- freqtrade/exchange/__init__.py | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index b0719bcc6..6134e9cad 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -36,7 +36,8 @@ "order_types": { "buy": "limit", "sell": "limit", - "stoploss": "market" + "stoploss": "market", + "stoploss_on_exchange": "false" }, "exchange": { "name": "bittrex", diff --git a/docs/configuration.md b/docs/configuration.md index 62559a41e..03059e261 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -39,7 +39,7 @@ The table below will list all configuration parameters. | `ask_strategy.use_order_book` | false | No | Allows selling of open traded pair using the rates in Order Book Asks. | `ask_strategy.order_book_min` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. | `ask_strategy.order_book_max` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. -| `order_types` | None | No | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`). +| `order_types` | None | No | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). | `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). | `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode. | `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode. @@ -141,17 +141,18 @@ end up paying more then would probably have been necessary. ### Understand order_types -`order_types` contains a dict mapping order-types to market-types. This allows to buy using limit orders, sell using limit-orders, and create stoploss orders using market. +`order_types` contains a dict mapping order-types to market-types as well as stoploss on or off exchange type. This allows to buy using limit orders, sell using limit-orders, and create stoploss orders using market. It also allows to set the stoploss "on exchange" which means stoploss order would be placed immediately once the buy order is fulfilled. This can be set in the configuration or in the strategy. Configuration overwrites strategy configurations. -If this is configured, all 3 values (`"buy"`, `"sell"` and `"stoploss"`) need to be present, otherwise the bot warn about it and will fail to start. +If this is configured, all 4 values (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`) need to be present, otherwise the bot warn about it and will fail to start. The below is the default which is used if this is not configured in either Strategy or configuration. ``` json "order_types": { "buy": "limit", "sell": "limit", - "stoploss": "market" + "stoploss": "market", + "stoploss_on_exchange": "false" }, ``` diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 481a219d6..86067d395 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -13,7 +13,7 @@ DEFAULT_HYPEROPT = 'DefaultHyperOpts' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite://' UNLIMITED_STAKE_AMOUNT = 'unlimited' -REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss'] +REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 6e826794a..a311fd666 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -227,7 +227,7 @@ class Exchange(object): raise OperationalException( f'Exchange {self.name} does not support market orders.') - if order_types.get('stoploss_on_exchange', False): + if order_types.get('stoploss_on_exchange'): if self.name is not 'Binance': raise OperationalException( 'On exchange stoploss is not supported for %s.' % self.name From 92930b2343494c7c2aa99b6511fa91466f03fccd Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 19:03:28 +0100 Subject: [PATCH 57/76] test fixed --- freqtrade/constants.py | 2 +- freqtrade/tests/exchange/test_exchange.py | 2 +- freqtrade/tests/strategy/test_strategy.py | 13 +++++++------ user_data/strategies/test_strategy.py | 5 +++-- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 86067d395..f8fb91240 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -112,7 +112,7 @@ CONF_SCHEMA = { 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss_on_exchange': {'type': 'boolean'} }, - 'required': ['buy', 'sell', 'stoploss'] + 'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] }, 'exchange': {'$ref': '#/definitions/exchange'}, 'edge': {'$ref': '#/definitions/edge'}, diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 1a46ff001..6ad84585c 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -375,7 +375,7 @@ def test_validate_order_types(default_conf, mocker): type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False}) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - default_conf['order_types'] = {'buy': 'limit', 'sell': 'limit', 'stoploss': 'market'} + default_conf['order_types'] = {'buy': 'limit', 'sell': 'limit', 'stoploss': 'market', 'stoploss_on_exchange': 'false'} with pytest.raises(OperationalException, match=r'Exchange .* does not support market orders.'): diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index a38050f24..66d988075 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -188,7 +188,8 @@ def test_strategy_override_order_types(caplog): order_types = { 'buy': 'market', 'sell': 'limit', - 'stoploss': 'limit' + 'stoploss': 'limit', + 'stoploss_on_exchange': True, } config = { @@ -198,13 +199,13 @@ def test_strategy_override_order_types(caplog): resolver = StrategyResolver(config) assert resolver.strategy.order_types - for method in ['buy', 'sell', 'stoploss']: + for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']: assert resolver.strategy.order_types[method] == order_types[method] assert ('freqtrade.strategy.resolver', logging.INFO, "Override strategy 'order_types' with value in config file:" - " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit'}." + " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit', 'stoploss_on_exchange': True}." ) in caplog.record_tuples config = { @@ -262,13 +263,13 @@ def test_call_deprecated_function(result, monkeypatch): assert resolver.strategy._sell_fun_len == 2 indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata) - assert type(indicator_df) is DataFrame + assert isinstance(indicator_df, DataFrame) assert 'adx' in indicator_df.columns buydf = resolver.strategy.advise_buy(result, metadata=metadata) - assert type(buydf) is DataFrame + assert isinstance(buydf, DataFrame) assert 'buy' in buydf.columns selldf = resolver.strategy.advise_sell(result, metadata=metadata) - assert type(selldf) is DataFrame + assert isinstance(selldf, DataFrame) assert 'sell' in selldf diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py index fd2e9ab75..e7804e683 100644 --- a/user_data/strategies/test_strategy.py +++ b/user_data/strategies/test_strategy.py @@ -7,7 +7,7 @@ from pandas import DataFrame # Add your lib to import here import talib.abstract as ta import freqtrade.vendor.qtpylib.indicators as qtpylib -import numpy # noqa +import numpy # noqa # This class is a sample. Feel free to customize it. @@ -52,7 +52,8 @@ class TestStrategy(IStrategy): order_types = { 'buy': 'limit', 'sell': 'limit', - 'stoploss': 'market' + 'stoploss': 'market', + 'stoploss_on_exchange': False } def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: From a80c984323ef8dec3aa5468ef572eca0ae966260 Mon Sep 17 00:00:00 2001 From: misagh Date: Sun, 25 Nov 2018 19:09:11 +0100 Subject: [PATCH 58/76] flake8 --- freqtrade/tests/exchange/test_exchange.py | 7 ++++++- freqtrade/tests/strategy/test_strategy.py | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 6ad84585c..d7f70d477 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -375,7 +375,12 @@ def test_validate_order_types(default_conf, mocker): type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False}) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - default_conf['order_types'] = {'buy': 'limit', 'sell': 'limit', 'stoploss': 'market', 'stoploss_on_exchange': 'false'} + default_conf['order_types'] = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'market', + 'stoploss_on_exchange': 'false' + } with pytest.raises(OperationalException, match=r'Exchange .* does not support market orders.'): diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 66d988075..1ad774ffa 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -205,7 +205,8 @@ def test_strategy_override_order_types(caplog): assert ('freqtrade.strategy.resolver', logging.INFO, "Override strategy 'order_types' with value in config file:" - " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit', 'stoploss_on_exchange': True}." + " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit'," + " 'stoploss_on_exchange': True}." ) in caplog.record_tuples config = { From b2634e8e085d8510740bf16c1293067a8e8a6b73 Mon Sep 17 00:00:00 2001 From: misagh Date: Mon, 26 Nov 2018 18:28:13 +0100 Subject: [PATCH 59/76] typo corrected --- freqtrade/freqtradebot.py | 2 +- freqtrade/strategy/interface.py | 2 +- freqtrade/tests/test_freqtradebot.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d9a15f56a..5425e8736 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -700,7 +700,7 @@ class FreqtradeBot(object): 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_EXCHNAGE.value + trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.update(order) result = True else: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1073f8028..141dd996c 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -33,7 +33,7 @@ class SellType(Enum): """ ROI = "roi" STOP_LOSS = "stop_loss" - STOPLOSS_ON_EXCHNAGE = "stoploss_on_exchange" + STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange" TRAILING_STOP_LOSS = "trailing_stop_loss" SELL_SIGNAL = "sell_signal" FORCE_SELL = "force_sell" diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index b6b42d1da..131d7df99 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1685,7 +1685,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, assert trade.stoploss_order_id is None assert trade.is_open is False print(trade.sell_reason) - assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHNAGE.value + assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value assert rpc_mock.call_count == 1 From 17004a5a72a232b3c80389f09de97fe107439d98 Mon Sep 17 00:00:00 2001 From: misagh Date: Mon, 26 Nov 2018 18:29:41 +0100 Subject: [PATCH 60/76] documentation corrected --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 03059e261..e05405aed 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -152,7 +152,7 @@ The below is the default which is used if this is not configured in either Strat "buy": "limit", "sell": "limit", "stoploss": "market", - "stoploss_on_exchange": "false" + "stoploss_on_exchange": False }, ``` From 1f1770ad5a21e5c994fd46041b183fab6db2cc34 Mon Sep 17 00:00:00 2001 From: misagh Date: Mon, 26 Nov 2018 18:46:59 +0100 Subject: [PATCH 61/76] migration script and and error handling on stop loss order --- freqtrade/exchange/__init__.py | 19 ++++++++++++++++++- freqtrade/persistence.py | 2 +- freqtrade/tests/test_persistence.py | 1 + 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index a311fd666..38e2fa317 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -372,9 +372,26 @@ class Exchange(object): } return self._dry_run_open_orders[order_id] - return self._api.create_order(pair, 'stop_loss', 'sell', + try: + return self._api.create_order(pair, 'stop_loss', 'sell', amount, rate, {'stopPrice': stop_price}) + except ccxt.InsufficientFunds as e: + raise DependencyException( + f'Insufficient funds to place stoploss limit order on market {pair}.' + f'Tried to put a stoploss amount {amount} at rate {rate} (total {rate*amount}).' + f'Message: {e}') + except ccxt.InvalidOrder as e: + raise DependencyException( + f'Could not place stoploss limit order on market {pair}.' + f'Tried to place stoploss amount {amount} at rate {rate} (total {rate*amount}).' + f'Message: {e}') + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not place stoploss limit order due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) + @retrier def get_balance(self, currency: str) -> float: if self._conf['dry_run']: diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 364af06ce..26b0d9d93 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -82,7 +82,7 @@ def check_migrate(engine) -> None: logger.debug(f'trying {table_back_name}') # Check for latest column - if not has_column(cols, 'ticker_interval'): + if not has_column(cols, 'stoploss_order_id'): logger.info(f'Running database migration - backup available as {table_back_name}') fee_open = get_column_def(cols, 'fee_open', 'fee') diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 5e0647dff..cdfdef6e6 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -426,6 +426,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): max_rate FLOAT, sell_reason VARCHAR, strategy VARCHAR, + ticker_interval INTEGER, PRIMARY KEY (id), CHECK (is_open IN (0, 1)) );""" From b63535083e66a404724968c5edf6c5841c65b87e Mon Sep 17 00:00:00 2001 From: misagh Date: Mon, 26 Nov 2018 18:47:32 +0100 Subject: [PATCH 62/76] flake8 --- freqtrade/exchange/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 38e2fa317..64875af87 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -374,7 +374,7 @@ class Exchange(object): try: return self._api.create_order(pair, 'stop_loss', 'sell', - amount, rate, {'stopPrice': stop_price}) + amount, rate, {'stopPrice': stop_price}) except ccxt.InsufficientFunds as e: raise DependencyException( From 7f6fc7e90f4edde9757d3f794d396916d935da47 Mon Sep 17 00:00:00 2001 From: misagh Date: Mon, 26 Nov 2018 19:13:36 +0100 Subject: [PATCH 63/76] Lost in git ! --- freqtrade/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 7f4022d1f..3ed478ec3 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -45,7 +45,7 @@ def main(sysargv: List[str]) -> None: freqtrade = FreqtradeBot(config) state = None - while True: + while 1: state = freqtrade.worker(old_state=state) if state == State.RELOAD_CONF: freqtrade = reconfigure(freqtrade, args) From 6351fe7a7f927763aa79e28d941d60ab3e9a7fea Mon Sep 17 00:00:00 2001 From: misagh Date: Mon, 26 Nov 2018 20:24:13 +0100 Subject: [PATCH 64/76] test added: stoploss_order_id should be null after migration --- freqtrade/tests/test_persistence.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index cdfdef6e6..d0a209f40 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -472,6 +472,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert trade.sell_reason is None assert trade.strategy is None assert trade.ticker_interval is None + assert trade.stoploss_order_id is None assert log_has("trying trades_bak1", caplog.record_tuples) assert log_has("trying trades_bak2", caplog.record_tuples) assert log_has("Running database migration - backup available as trades_bak2", From 7dbf0fed684e9af10da8c2fda45a03b9f1a3bdcb Mon Sep 17 00:00:00 2001 From: misagh Date: Tue, 27 Nov 2018 17:09:51 +0100 Subject: [PATCH 65/76] stop loss limit order type corrected --- freqtrade/exchange/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index a2857a12a..2480dbe32 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -373,7 +373,7 @@ class Exchange(object): return self._dry_run_open_orders[order_id] try: - return self._api.create_order(pair, 'stop_loss', 'sell', + return self._api.create_order(pair, 'stop_loss_limit', 'sell', amount, rate, {'stopPrice': stop_price}) except ccxt.InsufficientFunds as e: From 29f680ec5d6f5bd837788b935e7b27dc3eee77f7 Mon Sep 17 00:00:00 2001 From: misagh Date: Tue, 27 Nov 2018 17:26:06 +0100 Subject: [PATCH 66/76] fix order type 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 63f1008b9..ec7c2acae 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1150,7 +1150,7 @@ def test_get_fee(default_conf, mocker): def test_stoploss_limit_order(default_conf, mocker): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) - order_type = 'stop_loss' + order_type = 'stop_loss_limit' api_mock.create_order = MagicMock(return_value={ 'id': order_id, From da94e97c602861492cf03a38eb37c0d6e382e94e Mon Sep 17 00:00:00 2001 From: misagh Date: Wed, 28 Nov 2018 13:58:53 +0100 Subject: [PATCH 67/76] in case trade is not open, then handle_stoploss_on_exchange should not be called --- freqtrade/freqtradebot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index aeb8ce50c..c72451df6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -554,7 +554,7 @@ class FreqtradeBot(object): trade.update(order) - if self.strategy.order_types.get('stoploss_on_exchange'): + if self.strategy.order_types.get('stoploss_on_exchange') and trade.is_open: result = self.handle_stoploss_on_exchange(trade) if result: self.wallets.update() @@ -675,7 +675,7 @@ class FreqtradeBot(object): # If trade is open and the buy order is fulfilled but there is no stoploss, # then we add a stoploss on exchange - if trade.is_open and not trade.open_order_id and not trade.stoploss_order_id: + if not trade.open_order_id and not trade.stoploss_order_id: if self.edge: stoploss = self.edge.stoploss(pair=trade.pair) else: @@ -692,7 +692,7 @@ class FreqtradeBot(object): )['id'] trade.stoploss_order_id = str(stoploss_order_id) - # Or there is already a stoploss on exchange. + # 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) From fb755880fad8cc2e0b0145544117b7243eac26dd Mon Sep 17 00:00:00 2001 From: misagh Date: Wed, 28 Nov 2018 14:16:50 +0100 Subject: [PATCH 68/76] logs added in case stop loss on exchange is hit --- freqtrade/persistence.py | 1 + freqtrade/tests/test_freqtradebot.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 26b0d9d93..64663a2fd 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -255,6 +255,7 @@ class Trade(_DECL_BASE): self.close(order['price']) elif order_type == 'stop_loss_limit': self.stoploss_order_id = None + logger.info('STOP_LOSS_LIMIT is hit for %s.', self) self.close(order['price']) else: raise ValueError(f'Unknown order type: {order_type}') diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 995a3e8ff..81ade608a 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -899,7 +899,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None assert trade.is_open is True -def test_handle_stoploss_on_exchange(mocker, default_conf, fee, +def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, markets, limit_buy_order, limit_sell_order) -> None: stoploss_limit = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) @@ -961,6 +961,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, }) mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True + assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog.record_tuples) assert trade.stoploss_order_id is None assert trade.is_open is False From c913fef80c5c31d8007b5d437e9d371196440695 Mon Sep 17 00:00:00 2001 From: misagh Date: Wed, 28 Nov 2018 15:45:11 +0100 Subject: [PATCH 69/76] =?UTF-8?q?stop=20loss=20limit=20when=20hit,=20the?= =?UTF-8?q?=20close=20price=20is=20=E2=80=9Caverage=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- freqtrade/persistence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 64663a2fd..592a88acb 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -256,7 +256,7 @@ class Trade(_DECL_BASE): elif order_type == 'stop_loss_limit': self.stoploss_order_id = None logger.info('STOP_LOSS_LIMIT is hit for %s.', self) - self.close(order['price']) + self.close(order['average']) else: raise ValueError(f'Unknown order type: {order_type}') cleanup() From 1a5465fb508ca20318e36913cfb0cd2f12a995b1 Mon Sep 17 00:00:00 2001 From: misagh Date: Wed, 28 Nov 2018 19:35:10 +0100 Subject: [PATCH 70/76] logs enriched in case of stop loss on exchange, test fixed --- freqtrade/exchange/__init__.py | 8 +++++--- freqtrade/tests/test_freqtradebot.py | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 2480dbe32..baa9d573d 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -378,13 +378,15 @@ class Exchange(object): except ccxt.InsufficientFunds as e: raise DependencyException( - f'Insufficient funds to place stoploss limit order on market {pair}.' - f'Tried to put a stoploss amount {amount} at rate {rate} (total {rate*amount}).' + f'Insufficient funds to place stoploss limit order on market {pair}. ' + f'Tried to put a stoploss amount {amount} with ' + f'stop {stop_price} and limit {rate} (total {rate*amount}).' f'Message: {e}') except ccxt.InvalidOrder as e: raise DependencyException( f'Could not place stoploss limit order on market {pair}.' - f'Tried to place stoploss amount {amount} at rate {rate} (total {rate*amount}).' + f'Tried to place stoploss amount {amount} with ' + f'stop {stop_price} and limit {rate} (total {rate*amount}).' f'Message: {e}') except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 81ade608a..147d47b72 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -957,7 +957,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, stoploss_order_hit = MagicMock(return_value={ 'status': 'closed', 'type': 'stop_loss_limit', - 'price': 2 + 'price': 3, + 'average': 2 }) mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True From efcec736b55205f732493c381a3d2671b9ae1fa4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Nov 2018 20:02:12 +0100 Subject: [PATCH 71/76] refactor startup_messages to rpc_manger this cleans up freqtradebot slightly --- freqtrade/freqtradebot.py | 34 +------------------------ freqtrade/rpc/rpc_manager.py | 34 ++++++++++++++++++++++++- freqtrade/tests/rpc/test_rpc_manager.py | 20 +++++++++++++++ 3 files changed, 54 insertions(+), 34 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7a974d385..0c6369ef4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -107,7 +107,7 @@ class FreqtradeBot(object): }) logger.info('Changing state to: %s', state.name) if state == State.RUNNING: - self._startup_messages() + self.rpc.startup_messages(self.config) if state == State.STOPPED: time.sleep(1) @@ -121,38 +121,6 @@ class FreqtradeBot(object): min_secs=min_secs) return state - def _startup_messages(self) -> None: - if self.config.get('dry_run', False): - self.rpc.send_msg({ - 'type': RPCMessageType.WARNING_NOTIFICATION, - 'status': 'Dry run is enabled. All trades are simulated.' - }) - stake_currency = self.config['stake_currency'] - stake_amount = self.config['stake_amount'] - minimal_roi = self.config['minimal_roi'] - ticker_interval = self.config['ticker_interval'] - exchange_name = self.config['exchange']['name'] - strategy_name = self.config.get('strategy', '') - self.rpc.send_msg({ - 'type': RPCMessageType.CUSTOM_NOTIFICATION, - 'status': f'*Exchange:* `{exchange_name}`\n' - f'*Stake per trade:* `{stake_amount} {stake_currency}`\n' - f'*Minimum ROI:* `{minimal_roi}`\n' - f'*Ticker Interval:* `{ticker_interval}`\n' - f'*Strategy:* `{strategy_name}`' - }) - if self.config.get('dynamic_whitelist', False): - top_pairs = 'top volume ' + str(self.config.get('dynamic_whitelist', 20)) - specific_pairs = '' - else: - top_pairs = 'whitelisted' - specific_pairs = '\n' + ', '.join(self.config['exchange'].get('pair_whitelist', '')) - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'Searching for {top_pairs} {stake_currency} pairs to buy and sell...' - f'{specific_pairs}' - }) - def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any: """ Throttles the given callable that it diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 022578378..74a4e3bdc 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -4,7 +4,7 @@ This module contains class to manage RPC communications (Telegram, Slack, ...) import logging from typing import List, Dict, Any -from freqtrade.rpc import RPC +from freqtrade.rpc import RPC, RPCMessageType logger = logging.getLogger(__name__) @@ -51,3 +51,35 @@ class RPCManager(object): for mod in self.registered_modules: logger.debug('Forwarding message to rpc.%s', mod.name) mod.send_msg(msg) + + def startup_messages(self, config) -> None: + if config.get('dry_run', False): + self.send_msg({ + 'type': RPCMessageType.WARNING_NOTIFICATION, + 'status': 'Dry run is enabled. All trades are simulated.' + }) + stake_currency = config['stake_currency'] + stake_amount = config['stake_amount'] + minimal_roi = config['minimal_roi'] + ticker_interval = config['ticker_interval'] + exchange_name = config['exchange']['name'] + strategy_name = config.get('strategy', '') + self.send_msg({ + 'type': RPCMessageType.CUSTOM_NOTIFICATION, + 'status': f'*Exchange:* `{exchange_name}`\n' + f'*Stake per trade:* `{stake_amount} {stake_currency}`\n' + f'*Minimum ROI:* `{minimal_roi}`\n' + f'*Ticker Interval:* `{ticker_interval}`\n' + f'*Strategy:* `{strategy_name}`' + }) + if config.get('dynamic_whitelist', False): + top_pairs = 'top volume ' + str(config.get('dynamic_whitelist', 20)) + specific_pairs = '' + else: + top_pairs = 'whitelisted' + specific_pairs = '\n' + ', '.join(config['exchange'].get('pair_whitelist', '')) + self.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f'Searching for {top_pairs} {stake_currency} pairs to buy and sell...' + f'{specific_pairs}' + }) diff --git a/freqtrade/tests/rpc/test_rpc_manager.py b/freqtrade/tests/rpc/test_rpc_manager.py index 90c693830..cbb858522 100644 --- a/freqtrade/tests/rpc/test_rpc_manager.py +++ b/freqtrade/tests/rpc/test_rpc_manager.py @@ -113,3 +113,23 @@ def test_init_webhook_enabled(mocker, default_conf, caplog) -> None: assert log_has('Enabling rpc.webhook ...', caplog.record_tuples) assert len(rpc_manager.registered_modules) == 1 assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules] + + +def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: + telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + rpc_manager = RPCManager(freqtradebot) + rpc_manager.startup_messages(default_conf) + + assert telegram_mock.call_count == 3 + assert "*Exchange:* `bittrex`" in telegram_mock.call_args_list[1][0][0]['status'] + + telegram_mock.reset_mock() + default_conf['dry_run'] = True + default_conf['dynamic_whitelist'] = 20 + + rpc_manager.startup_messages(default_conf) + assert telegram_mock.call_count == 3 + assert "Dry run is enabled." in telegram_mock.call_args_list[0][0][0]['status'] From 42c8888fa1967bf024ed8afb0e4414cb66e142ee Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 30 Nov 2018 13:34:08 +0100 Subject: [PATCH 72/76] Update ccxt from 1.17.556 to 1.17.563 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e2a1332ba..f7db2ea09 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.556 +ccxt==1.17.563 SQLAlchemy==1.2.14 python-telegram-bot==11.1.0 arrow==0.12.1 From f554647efd8e072ddea0bd86b6e9abb33e40daca Mon Sep 17 00:00:00 2001 From: misagh Date: Fri, 30 Nov 2018 14:14:31 +0100 Subject: [PATCH 73/76] =?UTF-8?q?=E2=80=9Cchecking=20sell=E2=80=9D=20messa?= =?UTF-8?q?ge=20removed=20to=20debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7a974d385..a9a51aae0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -648,7 +648,7 @@ class FreqtradeBot(object): return True break else: - logger.info('checking sell') + logger.debug('checking sell') if self.check_sell(trade, sell_rate, buy, sell): return True From f04655c012c90ceffb01a562e80a8d8c72f6e10c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Nov 2018 20:13:50 +0100 Subject: [PATCH 74/76] Test exceptions in sell-stoploss --- freqtrade/tests/exchange/test_exchange.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index ec7c2acae..d1f391266 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1182,6 +1182,27 @@ def test_stoploss_limit_order(default_conf, mocker): assert api_mock.create_order.call_args[0][4] == 200 assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220} + # test exception handling + with pytest.raises(DependencyException): + api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds) + 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) + 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) + 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) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + def test_stoploss_limit_order_dry_run(default_conf, mocker): api_mock = MagicMock() From d4f83a7516c916ea65e2013f632a75f0686c56c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Nov 2018 20:15:56 +0100 Subject: [PATCH 75/76] Fix missing mock in test_add_stoploss_on_exchange --- 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 147d47b72..a3638d08a 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -876,6 +876,7 @@ def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> Non def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None: patch_RPCManager(mocker) + patch_exchange(mocker) 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=[]) From 24f573f3b01e3e59eafc856b03dbf57d38d61a9a Mon Sep 17 00:00:00 2001 From: Misagh Date: Sat, 1 Dec 2018 10:01:11 +0100 Subject: [PATCH 76/76] log "Found no sell signal for whitelisted ..." changed (#1378) * sell log enriched and put modify on debug --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 62b0a0d2c..d85a533b7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -629,7 +629,7 @@ class FreqtradeBot(object): if self.check_sell(trade, sell_rate, buy, sell): return True - logger.info('Found no sell signals for whitelisted currencies. Trying again..') + logger.debug('Found no sell signal for %s.', trade) return False def handle_stoploss_on_exchange(self, trade: Trade) -> bool: