From 1936dd1ee8a0c3187dfec1216909c71572ae1205 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 15:45:07 +0200 Subject: [PATCH 01/17] Add test-case verifying "changing" wallet with unlimited amount --- tests/test_wallets.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 562957790..86f49698b 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -121,13 +121,14 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) -@pytest.mark.parametrize("balance_ratio,result1", [ - (1, 50), - (0.99, 49.5), - (0.50, 25), +@pytest.mark.parametrize("balance_ratio,result1,result2", [ + (1, 50, 66.66666), + (0.99, 49.5, 66.0), + (0.50, 25, 33.3333), ]) def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, - limit_buy_order_open, fee, mocker) -> None: + result2, limit_buy_order_open, + fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, @@ -150,7 +151,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' freqtrade.execute_buy('ETH/USDT', result) - result = freqtrade.wallets.get_trade_stake_amount('LTC/USDDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT', freqtrade.get_free_open_trades()) assert result == result1 # create 2 trades, order amount should be None @@ -159,6 +160,12 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) assert result == 0 + freqtrade.config['max_open_trades'] = 3 + freqtrade.config['dry_run_wallet'] = 200 + freqtrade.wallets.start_cap = 200 + result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) + assert round(result, 4) == round(result2, 4) + # set max_open_trades = None, so do not trade freqtrade.config['max_open_trades'] = 0 result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT', freqtrade.get_free_open_trades()) From 06d6f9ac41b1425a19062074ba6a0f945a98788f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 15:55:48 +0200 Subject: [PATCH 02/17] Fix calculation of unlimited_stake in case of modified wallet --- freqtrade/wallets.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index f4432e932..889fe6fa8 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -130,14 +130,13 @@ class Wallets: def get_all_balances(self) -> Dict[str, Any]: return self._wallets - def _get_available_stake_amount(self) -> float: + def _get_available_stake_amount(self, val_tied_up: float) -> float: """ Return the total currently available balance in stake currency, respecting tradable_balance_ratio. Calculated as - ( + free amount ) * tradable_balance_ratio - + ( + free amount) * tradable_balance_ratio - """ - val_tied_up = Trade.total_open_trades_stakes() # Ensure % is used from the overall balance # Otherwise we'd risk lowering stakes with each open trade. @@ -151,12 +150,13 @@ class Wallets: Calculate stake amount for "unlimited" stake amount :return: 0 if max number of trades reached, else stake_amount to use. """ - if not free_open_trades: + if not free_open_trades or self._config['max_open_trades'] == 0: return 0 - available_amount = self._get_available_stake_amount() + val_tied_up = Trade.total_open_trades_stakes() + available_amount = self._get_available_stake_amount(val_tied_up) - return available_amount / free_open_trades + return (available_amount + val_tied_up) / self._config['max_open_trades'] def _check_available_stake_amount(self, stake_amount: float) -> float: """ @@ -165,7 +165,8 @@ class Wallets: :return: float: Stake amount :raise: DependencyException if balance is lower than stake-amount """ - available_amount = self._get_available_stake_amount() + val_tied_up = Trade.total_open_trades_stakes() + available_amount = self._get_available_stake_amount(val_tied_up) if self._config['amend_last_stake_amount']: # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio From 0233aa248e6fc9edfaa9567e7dd50941616d4d0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Apr 2021 17:22:16 +0200 Subject: [PATCH 03/17] Limit stake_amount to max available amount --- freqtrade/wallets.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 889fe6fa8..4415e4d53 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -156,7 +156,9 @@ class Wallets: val_tied_up = Trade.total_open_trades_stakes() available_amount = self._get_available_stake_amount(val_tied_up) - return (available_amount + val_tied_up) / self._config['max_open_trades'] + # Theoretical amount can be above available amount - therefore limit to available amount! + return min((available_amount + val_tied_up) / self._config['max_open_trades'], + available_amount) def _check_available_stake_amount(self, stake_amount: float) -> float: """ From ba2d4d4656d2ecc929bd517786a2c0e325ea7c45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Apr 2021 19:27:21 +0200 Subject: [PATCH 04/17] Reduce number of calls to `Trade.total_open_traes_stakes()` --- freqtrade/wallets.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 4415e4d53..dba16cc35 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -145,7 +145,8 @@ class Wallets: self._config['tradable_balance_ratio']) - val_tied_up return available_amount - def _calculate_unlimited_stake_amount(self, free_open_trades: int) -> float: + def _calculate_unlimited_stake_amount(self, free_open_trades: int, available_amount: float, + val_tied_up: float) -> float: """ Calculate stake amount for "unlimited" stake amount :return: 0 if max number of trades reached, else stake_amount to use. @@ -153,22 +154,17 @@ class Wallets: if not free_open_trades or self._config['max_open_trades'] == 0: return 0 - val_tied_up = Trade.total_open_trades_stakes() - available_amount = self._get_available_stake_amount(val_tied_up) - + possible_stake = (available_amount + val_tied_up) / self._config['max_open_trades'] # Theoretical amount can be above available amount - therefore limit to available amount! - return min((available_amount + val_tied_up) / self._config['max_open_trades'], - available_amount) + return min(possible_stake, available_amount) - def _check_available_stake_amount(self, stake_amount: float) -> float: + def _check_available_stake_amount(self, stake_amount: float, available_amount: float) -> float: """ Check if stake amount can be fulfilled with the available balance for the stake currency :return: float: Stake amount :raise: DependencyException if balance is lower than stake-amount """ - val_tied_up = Trade.total_open_trades_stakes() - available_amount = self._get_available_stake_amount(val_tied_up) if self._config['amend_last_stake_amount']: # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio @@ -195,17 +191,20 @@ class Wallets: stake_amount: float # Ensure wallets are uptodate. self.update() + val_tied_up = Trade.total_open_trades_stakes() + available_amount = self._get_available_stake_amount(val_tied_up) if edge: stake_amount = edge.stake_amount( pair, self.get_free(self._config['stake_currency']), self.get_total(self._config['stake_currency']), - Trade.total_open_trades_stakes() + val_tied_up ) else: stake_amount = self._config['stake_amount'] if stake_amount == UNLIMITED_STAKE_AMOUNT: - stake_amount = self._calculate_unlimited_stake_amount(free_open_trades) + stake_amount = self._calculate_unlimited_stake_amount( + free_open_trades, available_amount, val_tied_up) - return self._check_available_stake_amount(stake_amount) + return self._check_available_stake_amount(stake_amount, available_amount) From d8c8a8d8c22923c301f8db7e1099a5422c5fe026 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Apr 2021 20:01:10 +0200 Subject: [PATCH 05/17] Remvoe pointless arguments from get_trade_stake_amount --- freqtrade/freqtradebot.py | 3 +-- freqtrade/optimize/backtesting.py | 8 +++----- freqtrade/rpc/rpc.py | 3 +-- freqtrade/wallets.py | 8 ++++---- tests/optimize/test_backtesting.py | 10 +++++----- tests/test_freqtradebot.py | 16 ++++++---------- tests/test_integration.py | 6 ++---- tests/test_wallets.py | 12 ++++++------ 8 files changed, 28 insertions(+), 38 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1ebf28ebd..f370ff34f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -472,8 +472,7 @@ class FreqtradeBot(LoggingMixin): (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df) if buy and not sell: - stake_amount = self.wallets.get_trade_stake_amount(pair, self.get_free_open_trades(), - self.edge) + stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) if not stake_amount: logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") return False diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ff1dd934c..71110b914 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -273,11 +273,9 @@ class Backtesting: return None - def _enter_trade(self, pair: str, row: List, max_open_trades: int, - open_trade_count: int) -> Optional[LocalTrade]: + def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]: try: - stake_amount = self.wallets.get_trade_stake_amount( - pair, max_open_trades - open_trade_count, None) + stake_amount = self.wallets.get_trade_stake_amount(pair, None) except DependencyException: return None min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) @@ -388,7 +386,7 @@ class Backtesting: and tmp != end_date and row[BUY_IDX] == 1 and row[SELL_IDX] != 1 and not PairLocks.is_pair_locked(pair, row[DATE_IDX])): - trade = self._enter_trade(pair, row, max_open_trades, open_trade_count_start) + trade = self._enter_trade(pair, row) if trade: # TODO: hacky workaround to avoid opening > max_open_trades # This emulates previous behaviour - not sure if this is correct diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b86562e80..eedb1c510 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -603,8 +603,7 @@ class RPC: raise RPCException(f'position for {pair} already open - id: {trade.id}') # gen stake amount - stakeamount = self._freqtrade.wallets.get_trade_stake_amount( - pair, self._freqtrade.get_free_open_trades()) + stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair) # execute buy if self._freqtrade.execute_buy(pair, stakeamount, price, forcebuy=True): diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index dba16cc35..bbbe5ba5e 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -145,13 +145,13 @@ class Wallets: self._config['tradable_balance_ratio']) - val_tied_up return available_amount - def _calculate_unlimited_stake_amount(self, free_open_trades: int, available_amount: float, + def _calculate_unlimited_stake_amount(self, available_amount: float, val_tied_up: float) -> float: """ Calculate stake amount for "unlimited" stake amount :return: 0 if max number of trades reached, else stake_amount to use. """ - if not free_open_trades or self._config['max_open_trades'] == 0: + if self._config['max_open_trades'] == 0: return 0 possible_stake = (available_amount + val_tied_up) / self._config['max_open_trades'] @@ -182,7 +182,7 @@ class Wallets: return stake_amount - def get_trade_stake_amount(self, pair: str, free_open_trades: int, edge=None) -> float: + def get_trade_stake_amount(self, pair: str, edge=None) -> float: """ Calculate stake amount for the trade :return: float: Stake amount @@ -205,6 +205,6 @@ class Wallets: stake_amount = self._config['stake_amount'] if stake_amount == UNLIMITED_STAKE_AMOUNT: stake_amount = self._calculate_unlimited_stake_amount( - free_open_trades, available_amount, val_tied_up) + available_amount, val_tied_up) return self._check_available_stake_amount(stake_amount, available_amount) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 4bbfe8a78..41d4207c3 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -457,7 +457,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti Backtesting(default_conf) -def test_backtest__enter_trade(default_conf, fee, mocker, testdatadir) -> None: +def test_backtest__enter_trade(default_conf, fee, mocker) -> None: default_conf['ask_strategy']['use_sell_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) @@ -474,24 +474,24 @@ def test_backtest__enter_trade(default_conf, fee, mocker, testdatadir) -> None: 0.00099, # Low 0.0012, # High ] - trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + trade = backtesting._enter_trade(pair, row=row) assert isinstance(trade, LocalTrade) assert trade.stake_amount == 495 - trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=2) + trade = backtesting._enter_trade(pair, row=row) assert trade is None # Stake-amount too high! mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) - trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + trade = backtesting._enter_trade(pair, row=row) assert trade is None # Stake-amount too high! mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount", side_effect=DependencyException) - trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + trade = backtesting._enter_trade(pair, row=row) assert trade is None diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 433cce170..b0fa2d6c2 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -160,8 +160,7 @@ def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None: freqtrade = FreqtradeBot(default_conf) - result = freqtrade.wallets.get_trade_stake_amount( - 'ETH/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC') assert result == default_conf['stake_amount'] @@ -197,14 +196,12 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b if expected[i] is not None: limit_buy_order_open['id'] = str(i) - result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', - freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC') assert pytest.approx(result) == expected[i] freqtrade.execute_buy('ETH/BTC', result) else: with pytest.raises(DependencyException): - freqtrade.wallets.get_trade_stake_amount('ETH/BTC', - freqtrade.get_free_open_trades()) + freqtrade.wallets.get_trade_stake_amount('ETH/BTC') def test_edge_called_in_process(mocker, edge_conf) -> None: @@ -230,9 +227,9 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: freqtrade = FreqtradeBot(edge_conf) assert freqtrade.wallets.get_trade_stake_amount( - 'NEO/BTC', freqtrade.get_free_open_trades(), freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.20 + 'NEO/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.20 assert freqtrade.wallets.get_trade_stake_amount( - 'LTC/BTC', freqtrade.get_free_open_trades(), freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21 + 'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21 def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None: @@ -448,8 +445,7 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open, patch_get_signal(freqtrade) assert not freqtrade.create_trade('ETH/BTC') - assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades(), - freqtrade.edge) == 0 + assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0 def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee, diff --git a/tests/test_integration.py b/tests/test_integration.py index 1c60faa7b..be0dd1137 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -177,8 +177,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc trades = Trade.query.all() assert len(trades) == 4 - assert freqtrade.wallets.get_trade_stake_amount( - 'XRP/BTC', freqtrade.get_free_open_trades()) == result1 + assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC') == result1 rpc._rpc_forcebuy('TKN/BTC', None) @@ -199,8 +198,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc # One trade sold assert len(trades) == 4 # stake-amount should now be reduced, since one trade was sold at a loss. - assert freqtrade.wallets.get_trade_stake_amount( - 'XRP/BTC', freqtrade.get_free_open_trades()) < result1 + assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC') < result1 # Validate that balance of sold trade is not in dry-run balances anymore. bals2 = freqtrade.wallets.get_all_balances() assert bals != bals2 diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 86f49698b..ff303e2ec 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -118,7 +118,7 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) with pytest.raises(DependencyException, match=r'.*stake amount.*'): - freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) + freqtrade.wallets.get_trade_stake_amount('ETH/BTC') @pytest.mark.parametrize("balance_ratio,result1,result2", [ @@ -145,28 +145,28 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r freqtrade = get_patched_freqtradebot(mocker, conf) # no open trades, order amount should be 'balance / max_open_trades' - result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT') assert result == result1 # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' freqtrade.execute_buy('ETH/USDT', result) - result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT') assert result == result1 # create 2 trades, order amount should be None freqtrade.execute_buy('LTC/BTC', result) - result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT') assert result == 0 freqtrade.config['max_open_trades'] = 3 freqtrade.config['dry_run_wallet'] = 200 freqtrade.wallets.start_cap = 200 - result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT') assert round(result, 4) == round(result2, 4) # set max_open_trades = None, so do not trade freqtrade.config['max_open_trades'] = 0 - result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT') assert result == 0 From 92a2e254af2c0182840830f3c0f2b9ba10d566cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Apr 2021 20:17:30 +0200 Subject: [PATCH 06/17] Fix backtesting test --- tests/optimize/test_backtesting.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 41d4207c3..00114be5b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -463,6 +463,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) patch_exchange(mocker) default_conf['stake_amount'] = 'unlimited' + default_conf['max_open_trades'] = 2 backtesting = Backtesting(default_conf) pair = 'UNITTEST/BTC' row = [ @@ -478,8 +479,14 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: assert isinstance(trade, LocalTrade) assert trade.stake_amount == 495 + # Fake 2 trades, so there's not enough amount for the next trade left. + LocalTrade.trades_open.append(trade) + LocalTrade.trades_open.append(trade) trade = backtesting._enter_trade(pair, row=row) assert trade is None + LocalTrade.trades_open.pop() + trade = backtesting._enter_trade(pair, row=row) + assert trade is not None # Stake-amount too high! mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) @@ -487,7 +494,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: trade = backtesting._enter_trade(pair, row=row) assert trade is None - # Stake-amount too high! + # Stake-amount throwing error mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount", side_effect=DependencyException) From 9dc7f776d98a0e1db3412b1baa76ed8c93055d5e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Apr 2021 20:35:30 +0200 Subject: [PATCH 07/17] Improve log output when loading parameters --- freqtrade/strategy/hyper.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 16b576a73..988eae531 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -260,12 +260,15 @@ class HyperStrategyMixin(object): :param params: Dictionary with new parameter values. """ if not params: - return + logger.info(f"No params for {space} found, using default values.") + for attr_name, attr in self.enumerate_parameters(): - if attr_name in params: + if params and attr_name in params: if attr.load: attr.value = params[attr_name] logger.info(f'Strategy Parameter: {attr_name} = {attr.value}') else: logger.warning(f'Parameter "{attr_name}" exists, but is disabled. ' f'Default value "{attr.value}" used.') + else: + logger.info(f'Strategy Parameter(default): {attr_name} = {attr.value}') From 90476c4287aebacc7dc148950f420cffe8e8f038 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 07:00:33 +0200 Subject: [PATCH 08/17] Add "range" property to IntParameter --- freqtrade/strategy/hyper.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 988eae531..32486136d 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -5,7 +5,7 @@ This module defines a base class for auto-hyperoptable strategies. import logging from abc import ABC, abstractmethod from contextlib import suppress -from typing import Any, Iterator, Optional, Sequence, Tuple, Union +from typing import Any, Dict, Iterator, Optional, Sequence, Tuple, Union with suppress(ImportError): @@ -13,6 +13,7 @@ with suppress(ImportError): from freqtrade.optimize.space import SKDecimal from freqtrade.exceptions import OperationalException +from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -25,6 +26,7 @@ class BaseParameter(ABC): category: Optional[str] default: Any value: Any + hyperopt: bool = False def __init__(self, *, default: Any, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): @@ -121,6 +123,20 @@ class IntParameter(NumericParameter): """ return Integer(low=self.low, high=self.high, name=name, **self._space_params) + @property + def range(self): + """ + Get each value in this space as list. + Returns a List from low to high (inclusive) in Hyperopt mode. + Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid + calculating 100ds of indicators. + """ + if self.hyperopt: + # Scikit-optimize ranges are "inclusive", while python's "range" is exclusive + return range(self.low, self.high + 1) + else: + return range(self.value, self.value + 1) + class RealParameter(NumericParameter): default: float @@ -227,12 +243,11 @@ class HyperStrategyMixin(object): strategy logic. """ - def __init__(self, *args, **kwargs): + def __init__(self, config: Dict[str, Any], *args, **kwargs): """ Initialize hyperoptable strategy mixin. """ - self._load_params(getattr(self, 'buy_params', None)) - self._load_params(getattr(self, 'sell_params', None)) + self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT) def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]: """ @@ -254,7 +269,14 @@ class HyperStrategyMixin(object): (attr_name.startswith(category + '_') and attr.category is None)): yield attr_name, attr - def _load_params(self, params: dict) -> None: + def _load_hyper_params(self, hyperopt: bool = False) -> None: + """ + Load Hyperoptable parameters + """ + self._load_params(getattr(self, 'buy_params', None), 'buy', hyperopt) + self._load_params(getattr(self, 'sell_params', None), 'sell', hyperopt) + + def _load_params(self, params: dict, space: str, hyperopt: bool = False) -> None: """ Set optimizeable parameter values. :param params: Dictionary with new parameter values. @@ -263,6 +285,7 @@ class HyperStrategyMixin(object): logger.info(f"No params for {space} found, using default values.") for attr_name, attr in self.enumerate_parameters(): + attr.hyperopt = hyperopt if params and attr_name in params: if attr.load: attr.value = params[attr_name] From 5c7f278c8a21a7744978acd2ec4f8c93abffe1f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 07:18:35 +0200 Subject: [PATCH 09/17] add tests for IntParameter.range --- tests/optimize/conftest.py | 2 ++ tests/optimize/test_hyperopt.py | 9 +++++++++ tests/strategy/test_interface.py | 8 ++++++++ 3 files changed, 19 insertions(+) diff --git a/tests/optimize/conftest.py b/tests/optimize/conftest.py index 5c789ec1e..11b4674f3 100644 --- a/tests/optimize/conftest.py +++ b/tests/optimize/conftest.py @@ -6,6 +6,7 @@ import pandas as pd import pytest from freqtrade.optimize.hyperopt import Hyperopt +from freqtrade.state import RunMode from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange @@ -15,6 +16,7 @@ def hyperopt_conf(default_conf): hyperconf = deepcopy(default_conf) hyperconf.update({ 'datadir': Path(default_conf['datadir']), + 'runmode': RunMode.HYPEROPT, 'hyperopt': 'DefaultHyperOpt', 'hyperopt_loss': 'ShortTradeDurHyperOptLoss', 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 59bc4aefb..f725a5581 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -21,6 +21,7 @@ from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.space import SKDecimal from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode +from freqtrade.strategy.hyper import IntParameter from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -1103,6 +1104,14 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir) -> None: }) hyperopt = Hyperopt(hyperopt_conf) assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) + assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) + + assert hyperopt.backtesting.strategy.buy_rsi.hyperopt is True + assert hyperopt.backtesting.strategy.buy_rsi.value == 35 + buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range + assert isinstance(buy_rsi_range, range) + # Range from 0 - 50 (inclusive) + assert len(list(buy_rsi_range)) == 51 hyperopt.start() diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 78fa368e4..347d35b19 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -588,6 +588,14 @@ def test_hyperopt_parameters(): intpar = IntParameter(low=0, high=5, default=1, space='buy') assert intpar.value == 1 assert isinstance(intpar.get_space(''), Integer) + assert isinstance(intpar.range, range) + assert len(list(intpar.range)) == 1 + # Range contains ONLY the default / value. + assert list(intpar.range) == [intpar.value] + intpar.hyperopt = True + + assert len(list(intpar.range)) == 6 + assert list(intpar.range) == [0, 1, 2, 3, 4, 5] fltpar = RealParameter(low=0.0, high=5.5, default=1.0, space='buy') assert isinstance(fltpar.get_space(''), Real) From d647b841f0191b4a3c2a031452b96ebe1306403d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 09:03:59 +0200 Subject: [PATCH 10/17] Add docs how to optimize indicator parameters --- docs/hyperopt.md | 179 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 139 insertions(+), 40 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 51905e616..bea8dc256 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -165,11 +165,22 @@ Rarely you may also need to create a [nested class](advanced-hyperopt.md#overrid !!! Tip "Quickly optimize ROI, stoploss and trailing stoploss" You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything in your strategy. - ```python + ``` bash # Have a working strategy at hand. freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 ``` +### Hyperopt execution logic + +Hyperopt will first load your data into memory and will then run `populate_indicators()` once per Pair to generate all indicators. + +Hyperopt will then spawn into different processes (number of processors, or `-j `), and run backtesting over and over again, changing the parameters that are part of the `--spaces` defined. + +For every new set of parameters, freqtrade will run first `populate_buy_trend()` followed by `populate_sell_trend()`, and then run the regular backtesting process to simulate trades. + +After backtesting, the results are passed into the [loss function](#loss-functions), which will evaluate if this result was better or worse than previous results. +Based on the loss function result, hyperopt will determine the next set of parameters to try in the next round of backtesting. + ### Configure your Guards and Triggers There are two places you need to change in your strategy file to add a new buy hyperopt for testing: @@ -188,59 +199,54 @@ There you have two different types of indicators: 1. `guards` and 2. `triggers`. Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning). Hyper-optimization will, for each epoch round, pick one trigger and possibly -multiple guards. The constructed strategy will be something like "*buy exactly when close price touches lower Bollinger band, BUT only if -ADX > 10*". - -```python -from freqtrade.strategy import IntParameter, IStrategy - -class MyAwesomeStrategy(IStrategy): - # If parameter is prefixed with `buy_` or `sell_` then specifying `space` parameter is optional - # and space is inferred from parameter name. - buy_adx_min = IntParameter(0, 100, default=10) - - def populate_buy_trend(self, dataframe: 'DataFrame', metadata: dict) -> 'DataFrame': - dataframe.loc[ - ( - (dataframe['adx'] > self.buy_adx_min.value) - ), 'buy'] = 1 - return dataframe -``` +multiple guards. #### Sell optimization Similar to the buy-signal above, sell-signals can also be optimized. Place the corresponding settings into the following methods -* Define the parameters at the class level hyperopt shall be optimizing. +* Define the parameters at the class level hyperopt shall be optimizing, either naming them `sell_*`, or by explicitly defining `space='sell'`. * Within `populate_sell_trend()` - use defined parameter values instead of raw constants. The configuration and rules are the same than for buy signals. -```python -class MyAwesomeStrategy(IStrategy): - # There is no strict parameter naming scheme. If you do not use `buy_` or `sell_` prefixes - - # please specify to which space parameter belongs using `space` parameter. Possible values: - # 'buy' or 'sell'. - adx_max = IntParameter(0, 100, default=50, space='sell') +## Solving a Mystery - def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - dataframe.loc[ - ( - (dataframe['adx'] < self.adx_max.value) - ), 'buy'] = 1 +Let's say you are curious: should you use MACD crossings or lower Bollinger Bands to trigger your buys. +And you also wonder should you use RSI or ADX to help with those buy decisions. +If you decide to use RSI or ADX, which values should I use for them? + +So let's use hyperparameter optimization to solve this mystery. + +### Defining indicators to be used + +We start by calculating the indicators our strategy is going to use. + +``` python +class MyAwesomeStrategy(IStrategy): + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Generate all indicators used by the strategy + """ + dataframe['adx'] = ta.ADX(dataframe) + dataframe['rsi'] = ta.RSI(dataframe) + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + bollinger = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0) + dataframe['bb_lowerband'] = boll['lowerband'] + dataframe['bb_middleband'] = boll['middleband'] + dataframe['bb_upperband'] = boll['upperband'] return dataframe ``` -## Solving a Mystery +### Hyperoptable parameters -Let's say you are curious: should you use MACD crossings or lower Bollinger -Bands to trigger your buys. And you also wonder should you use RSI or ADX to -help with those buy decisions. If you decide to use RSI or ADX, which values -should I use for them? So let's use hyperparameter optimization to solve this -mystery. - -We will start by defining hyperoptable parameters: +We continue to define hyperoptable parameters: ```python class MyAwesomeStrategy(IStrategy): @@ -260,6 +266,8 @@ The last one we call `trigger` and use it to decide which buy trigger we want to So let's write the buy strategy using these values: ```python + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: conditions = [] # GUARDS AND TRENDS @@ -288,7 +296,7 @@ So let's write the buy strategy using these values: ``` Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. -It will use the given historical data and make buys based on the buy signals generated with the above function. +It will use the given historical data and simulate buys based on the buy signals generated with the above function. Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)). !!! Note @@ -314,6 +322,87 @@ There are four parameter types each suited for different purposes. !!! Warning Hyperoptable parameters cannot be used in `populate_indicators` - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case. +### Optimizing an indicator parameter + +Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy. + +``` python +import talib.abstract as ta + +from freqtrade.strategy import IStrategy +from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter +import freqtrade.vendor.qtpylib.indicators as qtpylib + +class MyAwesomeStrategy(IStrategy): + stoploss = 0.5 + timeframe = '15m' + # Define the parameter spaces + buy_ema_short = IntParameter(3, 50, default=5) + buy_ema_long = IntParameter(15, 200, default=50) + + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """Generate all indicators used by the strategy""" + + # Calculate all ema_short values + for val in self.buy_ema_short.range: + dataframe[f'ema_short_{val}'] = ta.EMA(dataframe, timeperiod=val) + + # Calculate all ema_long values + for val in self.buy_ema_long.range: + dataframe[f'ema_long_{val}'] = ta.EMA(dataframe, timeperiod=val) + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + conditions = [] + conditions.append(qtpylib.crossed_above( + dataframe[f'ema_short_{self.buy_ema_short.value}'], dataframe[f'ema_long_{self.buy_ema_long.value}'] + )) + + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + conditions = [] + conditions.append(qtpylib.crossed_above( + dataframe[f'ema_long_{self.buy_ema_long.value}'], dataframe[f'ema_short_{self.buy_ema_short.value}'] + )) + + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'sell'] = 1 + return dataframe +``` + +Breaking it down: + +Using `self.buy_ema_short.range` will return a range object containing all entries between the Parameters low and high value. +In this case (`IntParameter(3, 50, default=5)`), the loop would run for all numbers between 3 and 50 (`[3, 4, 5, ... 49, 50]`). +By using this in a loop, hyperopt will generate 48 new columns (`['buy_ema_3', 'buy_ema_4', ... , 'buy_ema_50']`). + +Hyperopt itself will then use the selected value to create the buy and sell signals + +While this strategy is most likely too simple to provide consistent profit, it should serve as an example how optimize indicator parameters. + +!!! Note + `self.buy_ema_short.range` will act differently between hyperopt and other modes. For hyperopt, the above example may generate 48 new columns, however for all other modes (backtesting, dry/live), it will only generate the column for the selected value. You should therefore avoid using the resulting column with explicit values (values other than `self.buy_ema_short.value`). + +??? Hint "Performance tip" + By doing the calculation of all possible indicators in `populate_indicators()`, the calculation of the indicator happens only once for every parameter. + While this may slow down the hyperopt startup speed, the overall performance will increase as the Hyperopt execution itself may pick the same value for multiple epochs (changing other values). + You should however try to use space ranges as small as possible. Every new column will require more memory, and every possibility hyperopt can try will increase the search space. + ## Loss-functions Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. @@ -606,6 +695,16 @@ number). You can also enable position stacking in the configuration file by explicitly setting `"position_stacking"=true`. +## Out of Memory errors + +As hyperopt consumes a lot of memory (the complete data needs to be in memory once per parallel backtesting process), it's likely that you run into "out of memory" errors. +To combat these, you have multiple options: + +* reduce the amount of pairs +* reduce the timerange used (`--timerange `) +* reduce the number of parallel processes (`-j `) +* Increase the memory of your machine + ## Show details of Hyperopt results After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. From 7453dac668274f02dbff38a0d6dea69197b3b9f5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 13:13:41 +0200 Subject: [PATCH 11/17] Improve doc wording --- docs/hyperopt.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index bea8dc256..b3fdc699b 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -198,8 +198,7 @@ There you have two different types of indicators: 1. `guards` and 2. `triggers`. However, this guide will make this distinction to make it clear that signals should not be "sticking". Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning). -Hyper-optimization will, for each epoch round, pick one trigger and possibly -multiple guards. +Hyper-optimization will, for each epoch round, pick one trigger and possibly multiple guards. #### Sell optimization @@ -266,8 +265,6 @@ The last one we call `trigger` and use it to decide which buy trigger we want to So let's write the buy strategy using these values: ```python - - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: conditions = [] # GUARDS AND TRENDS @@ -327,6 +324,9 @@ There are four parameter types each suited for different purposes. Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy. ``` python +from pandas import DataFrame +from functools import reduce + import talib.abstract as ta from freqtrade.strategy import IStrategy @@ -334,7 +334,7 @@ from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParame import freqtrade.vendor.qtpylib.indicators as qtpylib class MyAwesomeStrategy(IStrategy): - stoploss = 0.5 + stoploss = -0.05 timeframe = '15m' # Define the parameter spaces buy_ema_short = IntParameter(3, 50, default=5) From 31b0e3b5e8cc672c7a94a0e708b91a96e870dc9e Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Sat, 24 Apr 2021 13:25:28 +0200 Subject: [PATCH 12/17] add distribution graph to example notebook --- .../templates/strategy_analysis_example.ipynb | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index 491afbdd7..0bc593e2d 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -282,6 +282,28 @@ "graph.show(renderer=\"browser\")\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot average profit per trade as distribution graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import plotly.figure_factory as ff\n", + "\n", + "hist_data = [trades.profit_ratio]\n", + "group_labels = ['profit_ratio'] # name of the dataset\n", + "\n", + "fig = ff.create_distplot(hist_data, group_labels,bin_size=0.01)\n", + "fig.show()\n" + ] + }, { "cell_type": "markdown", "metadata": {}, From 185d754b8bd827d59e4eafae3edc4f6fe85077f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 13:39:20 +0200 Subject: [PATCH 13/17] Improve documentation to suggest config-private.json --- docs/configuration.md | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 0ade558f1..37395c5ee 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -11,7 +11,16 @@ Per default, the bot loads the configuration from the `config.json` file, locate You can specify a different configuration file used by the bot with the `-c/--config` command line option. -In some advanced use cases, multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream. +Multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream. + +!!! Tip "Use multiple configuration files to keep secrets secret" + You can use a 2nd configuration file containing your secrets. That way you can share your "primary" configuration file, while still keeping your API keys for yourself. + + ``` bash + freqtrade trade --config user_data/config.json --config user_data/config-private.json <...> + ``` + The 2nd file should only specify what you intend to override. + If a key is in more than one of the configurations, then the "last specified configuration" wins (in the above example, `config-private.json`). If you used the [Quick start](installation.md/#quick-start) method for installing the bot, the installation script should have already created the default configuration file (`config.json`) for you. @@ -518,16 +527,27 @@ API Keys are usually only required for live trading (trading for real money, bot **Insert your Exchange API key (change them by fake api keys):** ```json -"exchange": { +{ + "exchange": { "name": "bittrex", "key": "af8ddd35195e9dc500b9a6f799f6f5c93d89193b", "secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5", - ... + //"password": "", // Optional, not needed by all exchanges) + // ... + } + //... } ``` You should also make sure to read the [Exchanges](exchanges.md) section of the documentation to be aware of potential configuration details specific to your exchange. +!!! Hint "Keep your secrets secret" + To keep your secrets secret, we recommend to use a 2nd configuration for your API keys. + Simply use the above snippet in a new configuration file (e.g. `config-private.json`) and keep your settings in this file. + You can then start the bot with `freqtrade trade --config user_data/config.json --config user_data/config-private.json <...>` to have your keys loaded. + + **NEVER** share your private configuration file or your exchange keys with anyone! + ### Using proxy with Freqtrade To use a proxy with freqtrade, add the kwarg `"aiohttp_trust_env"=true` to the `"ccxt_async_kwargs"` dict in the exchange section of the configuration. From b223775385684c0698a143538ad8acb9086730ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 15:56:53 +0200 Subject: [PATCH 14/17] Update "output" of jupyter notebook as well --- docs/strategy_analysis_example.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index 5c479aa0b..4c938500c 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -195,4 +195,18 @@ graph.show(renderer="browser") ``` +## Plot average profit per trade as distribution graph + + +```python +import plotly.figure_factory as ff + +hist_data = [trades.profit_ratio] +group_labels = ['profit_ratio'] # name of the dataset + +fig = ff.create_distplot(hist_data, group_labels,bin_size=0.01) +fig.show() + +``` + Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data. From 88f26971fa6fdfc7ac05868b509d4d301fb4be8a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 19:15:09 +0200 Subject: [PATCH 15/17] Use defaultdict for backtesting --- freqtrade/optimize/backtesting.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fc5c0fdd7..fb9826a23 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -352,7 +352,7 @@ class Backtesting: data: Dict = self._get_ohlcv_as_lists(processed) # Indexes per pair, so some pairs are allowed to have a missing start. - indexes: Dict = {} + indexes: Dict = defaultdict(int) tmp = start_date + timedelta(minutes=self.timeframe_min) open_trades: Dict[str, List[LocalTrade]] = defaultdict(list) @@ -363,9 +363,6 @@ class Backtesting: open_trade_count_start = open_trade_count for i, pair in enumerate(data): - if pair not in indexes: - indexes[pair] = 0 - try: row = data[pair][indexes[pair]] except IndexError: From cb86c90d3ef4047c50f0fdb6392f03cc94cd86ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 19:16:54 +0200 Subject: [PATCH 16/17] Remove obsolete TODO's --- freqtrade/configuration/configuration.py | 2 -- freqtrade/exchange/exchange.py | 1 - 2 files changed, 3 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 86f337c1b..f6d0520c5 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -75,8 +75,6 @@ class Configuration: # Normalize config if 'internals' not in config: config['internals'] = {} - # TODO: This can be deleted along with removal of deprecated - # experimental settings if 'ask_strategy' not in config: config['ask_strategy'] = {} diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ed7918b36..80b392d73 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -363,7 +363,6 @@ class Exchange: invalid_pairs = [] for pair in extended_pairs: # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs - # TODO: add a support for having coins in BTC/USDT format if self.markets and pair not in self.markets: raise OperationalException( f'Pair {pair} is not available on {self.name}. ' From e855530483d7c2ff6cbb8c813ed050565833cbbb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 20:26:37 +0200 Subject: [PATCH 17/17] hdf5 handler should include the end-date --- freqtrade/data/history/hdf5datahandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index d116637e7..e80cfeba2 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -89,7 +89,7 @@ class HDF5DataHandler(IDataHandler): if timerange.starttype == 'date': where.append(f"date >= Timestamp({timerange.startts * 1e9})") if timerange.stoptype == 'date': - where.append(f"date < Timestamp({timerange.stopts * 1e9})") + where.append(f"date <= Timestamp({timerange.stopts * 1e9})") pairdata = pd.read_hdf(filename, key=key, mode="r", where=where)