From 09f0e7149f53986fbd6050ab87bbe79303737d58 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 2 Feb 2022 12:42:13 -0600 Subject: [PATCH 01/15] test__fetch_and_calculate_funding_fees_datetime_called # TODO-lev: test for longs --- tests/exchange/test_exchange.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index ae94ae102..69081bd83 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3774,6 +3774,8 @@ def test__fetch_and_calculate_funding_fees_datetime_called( # TODO-lev: test this for longs funding_fees = exchange._fetch_and_calculate_funding_fees('ADA/USDT', 30.0, True, d1) assert funding_fees == expected_fees + funding_fees = exchange._fetch_and_calculate_funding_fees('ADA/USDT', 30.0, False, d1) + assert funding_fees == 0 - expected_fees @pytest.mark.parametrize('pair,expected_size,trading_mode', [ From f58b92bb86aa43807258997d3dcf1aa3e074dd51 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 2 Feb 2022 13:15:42 -0600 Subject: [PATCH 02/15] exchange.create_order removed default for leverage --- freqtrade/exchange/exchange.py | 3 +- freqtrade/freqtradebot.py | 1 + freqtrade/persistence/models.py | 2 +- tests/exchange/test_exchange.py | 60 +++++++++++++++++++++------------ tests/exchange/test_kraken.py | 13 +++++-- 5 files changed, 52 insertions(+), 27 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2b35417d3..62f71b4ea 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -909,11 +909,10 @@ class Exchange: side: str, amount: float, rate: float, + leverage: float, reduceOnly: bool = False, - leverage: float = 1.0, time_in_force: str = 'gtc', ) -> Dict: - # TODO-lev: remove default for leverage if self._config['dry_run']: dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate, leverage) return dry_order diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6f39c23f7..d5f51a859 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1423,6 +1423,7 @@ class FreqtradeBot(LoggingMixin): side=trade.exit_side, amount=amount, rate=limit, + leverage=trade.leverage, reduceOnly=self.trading_mode == TradingMode.FUTURES, time_in_force=time_in_force ) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index afee0725f..f14bc9ae5 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -569,7 +569,7 @@ class LocalTrade(): payment = "BUY" if self.is_short else "SELL" # * On margin shorts, you buy a little bit more than the amount (amount + interest) logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.') - # TODO-lev: Double check this + # TODO-lev: Is anything else needed here? self.close(safe_value_fallback(order, 'average', 'price')) elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): self.stoploss_order_id = None diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 69081bd83..fae8bbece 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1213,7 +1213,8 @@ def test_buy_dry_run(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf) order = exchange.create_order(pair='ETH/BTC', ordertype='limit', side="buy", - amount=1, rate=200, time_in_force='gtc') + amount=1, rate=200, leverage=1.0, + time_in_force='gtc') assert 'id' in order assert 'dry_run_buy_' in order['id'] @@ -1238,7 +1239,8 @@ def test_buy_prod(default_conf, mocker, exchange_name): exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="buy", - amount=1, rate=200, time_in_force=time_in_force) + amount=1, rate=200, leverage=1.0, + time_in_force=time_in_force) assert 'id' in order assert 'info' in order @@ -1257,7 +1259,9 @@ def test_buy_prod(default_conf, mocker, exchange_name): side="buy", amount=1, rate=200, - time_in_force=time_in_force) + leverage=1.0, + time_in_force=time_in_force + ) 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] == 'buy' @@ -1269,31 +1273,36 @@ def test_buy_prod(default_conf, mocker, exchange_name): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("Not enough funds")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="buy", - amount=1, rate=200, time_in_force=time_in_force) + amount=1, rate=200, leverage=1.0, + time_in_force=time_in_force) with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.create_order(pair='ETH/BTC', ordertype='limit', side="buy", - amount=1, rate=200, time_in_force=time_in_force) + amount=1, rate=200, leverage=1.0, + time_in_force=time_in_force) with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.create_order(pair='ETH/BTC', ordertype='market', side="buy", - amount=1, rate=200, time_in_force=time_in_force) + amount=1, rate=200, leverage=1.0, + time_in_force=time_in_force) with pytest.raises(TemporaryError): api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("Network disconnect")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="buy", - amount=1, rate=200, time_in_force=time_in_force) + amount=1, rate=200, leverage=1.0, + time_in_force=time_in_force) with pytest.raises(OperationalException): api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="buy", - amount=1, rate=200, time_in_force=time_in_force) + amount=1, rate=200, leverage=1.0, + time_in_force=time_in_force) @pytest.mark.parametrize("exchange_name", EXCHANGES) @@ -1317,7 +1326,8 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): time_in_force = 'ioc' order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="buy", - amount=1, rate=200, time_in_force=time_in_force) + amount=1, rate=200, leverage=1.0, + time_in_force=time_in_force) assert 'id' in order assert 'info' in order @@ -1334,7 +1344,8 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): time_in_force = 'ioc' order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="buy", - amount=1, rate=200, time_in_force=time_in_force) + amount=1, rate=200, leverage=1.0, + time_in_force=time_in_force) assert 'id' in order assert 'info' in order @@ -1353,7 +1364,7 @@ def test_sell_dry_run(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf) order = exchange.create_order(pair='ETH/BTC', ordertype='limit', - side="sell", amount=1, rate=200) + side="sell", amount=1, rate=200, leverage=1.0) assert 'id' in order assert 'dry_run_sell_' in order['id'] @@ -1378,7 +1389,7 @@ def test_sell_prod(default_conf, mocker, exchange_name): exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, - side="sell", amount=1, rate=200) + side="sell", amount=1, rate=200, leverage=1.0) assert 'id' in order assert 'info' in order @@ -1392,7 +1403,8 @@ def test_sell_prod(default_conf, mocker, exchange_name): api_mock.create_order.reset_mock() order_type = 'limit' order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, - side="sell", amount=1, rate=200) + side="sell", amount=1, rate=200, + leverage=1.0) 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' @@ -1403,28 +1415,33 @@ def test_sell_prod(default_conf, mocker, exchange_name): with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", amount=1, rate=200) + exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", amount=1, rate=200, + leverage=1.0) with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.create_order(pair='ETH/BTC', ordertype='limit', side="sell", amount=1, rate=200) + exchange.create_order(pair='ETH/BTC', ordertype='limit', side="sell", amount=1, rate=200, + leverage=1.0) # Market orders don't require price, so the behaviour is slightly different with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.create_order(pair='ETH/BTC', ordertype='market', side="sell", amount=1, rate=200) + exchange.create_order(pair='ETH/BTC', ordertype='market', side="sell", amount=1, rate=200, + leverage=1.0) with pytest.raises(TemporaryError): api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No Connection")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", amount=1, rate=200) + exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", amount=1, rate=200, + leverage=1.0) with pytest.raises(OperationalException): api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", amount=1, rate=200) + exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", amount=1, rate=200, + leverage=1.0) @pytest.mark.parametrize("exchange_name", EXCHANGES) @@ -1448,7 +1465,8 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): time_in_force = 'ioc' order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", - amount=1, rate=200, time_in_force=time_in_force) + amount=1, rate=200, leverage=1.0, + time_in_force=time_in_force) assert 'id' in order assert 'info' in order @@ -1464,7 +1482,8 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): order_type = 'market' time_in_force = 'ioc' order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", - amount=1, rate=200, time_in_force=time_in_force) + amount=1, rate=200, leverage=1.0, + time_in_force=time_in_force) assert 'id' in order assert 'info' in order @@ -3771,7 +3790,6 @@ def test__fetch_and_calculate_funding_fees_datetime_called( d1 = datetime.strptime("2021-09-01 00:00:00 +0000", '%Y-%m-%d %H:%M:%S %z') time_machine.move_to("2021-09-01 08:00:00 +00:00") - # TODO-lev: test this for longs funding_fees = exchange._fetch_and_calculate_funding_fees('ADA/USDT', 30.0, True, d1) assert funding_fees == expected_fees funding_fees = exchange._fetch_and_calculate_funding_fees('ADA/USDT', 30.0, False, d1) diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index ff4200f8d..02df60990 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -32,8 +32,15 @@ def test_buy_kraken_trading_agreement(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") - order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="buy", - amount=1, rate=200, time_in_force=time_in_force) + order = exchange.create_order( + pair='ETH/BTC', + ordertype=order_type, + side="buy", + amount=1, + rate=200, + leverage=1.0, + time_in_force=time_in_force + ) assert 'id' in order assert 'info' in order @@ -66,7 +73,7 @@ def test_sell_kraken_trading_agreement(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, - side="sell", amount=1, rate=200) + side="sell", amount=1, rate=200, leverage=1.0) assert 'id' in order assert 'info' in order From bc6614df2d169473fb5d1b549dfaebd31cda4d1e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 2 Feb 2022 13:18:42 -0600 Subject: [PATCH 03/15] fixed failing tests in tests/commands/test_commands.py by adding NONE='' option to MarginMode --- freqtrade/constants.py | 2 +- freqtrade/enums/marginmode.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index fcf78c0e9..9c00dc7e3 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -46,7 +46,7 @@ DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume'] # it has wide consequences for stored trades files DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost'] TRADING_MODES = ['spot', 'margin', 'futures'] -MARGIN_MODES = ['cross', 'isolated'] +MARGIN_MODES = ['cross', 'isolated', ''] LAST_BT_RESULT_FN = '.last_result.json' FTHYPT_FILEVERSION = 'fthypt_fileversion' diff --git a/freqtrade/enums/marginmode.py b/freqtrade/enums/marginmode.py index 999051ca4..1e42809ea 100644 --- a/freqtrade/enums/marginmode.py +++ b/freqtrade/enums/marginmode.py @@ -9,3 +9,4 @@ class MarginMode(Enum): """ CROSS = "cross" ISOLATED = "isolated" + NONE = '' From 64bfa118e0069cbfdadfc467e6f915727138ecc9 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 2 Feb 2022 13:23:05 -0600 Subject: [PATCH 04/15] freqtradebot.handle_cancel_enter todo - Check edge cases, we dont want to make leverage > 1.0 if we dont have to --- freqtrade/freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d5f51a859..91468ee97 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1272,7 +1272,8 @@ class FreqtradeBot(LoggingMixin): # to the order dict acquired before cancelling. # we need to fall back to the values from order if corder does not contain these keys. trade.amount = filled_amount - # TODO-lev: Check edge cases, we don't want to make leverage > 1.0 if we don't have to + # * Check edge cases, we don't want to make leverage > 1.0 if we don't have to + # * (for leverage modes which aren't isolated futures) trade.stake_amount = trade.amount * trade.open_rate self.update_trade_state(trade, trade.open_order_id, corder) From 73d10b5c02c0de7a2136c312c2c00b6f7797b846 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 2 Feb 2022 13:30:05 -0600 Subject: [PATCH 05/15] backtesting._get_ohlcv_as_lists removed # TODO-lev: Candle-type should be conditional, either "spot" or futures --- freqtrade/optimize/backtesting.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6fac22cdf..68d71bc58 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -333,8 +333,12 @@ class Backtesting: df_analyzed.loc[:, col] = 0 if not tag_col else None # Update dataprovider cache - self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed, CandleType.SPOT) - # TODO-lev: Candle-type should be conditional, either "spot" or futures + self.dataprovider._set_cached_df( + pair, + self.timeframe, + df_analyzed, + CandleType.FUTURES if self.trading_mode == TradingMode.FUTURES else CandleType.SPOT + ) df_analyzed = df_analyzed.drop(df_analyzed.head(1).index) From c47c54c16c29a57ffc968341498b5e8afeb6212a Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 2 Feb 2022 13:35:34 -0600 Subject: [PATCH 06/15] removed strategy_test_v3.populate_sell_trend # TODO-lev: Add short logic, because it looked like the short logic was already there --- tests/strategy/strats/strategy_test_v3.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index d92c6c734..5d689e0a1 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -169,7 +169,6 @@ class StrategyTestV3(IStrategy): ), 'exit_short'] = 1 - # TODO-lev: Add short logic return dataframe def leverage(self, pair: str, current_time: datetime, current_rate: float, From 977f87659cbb6da29613b486d0b3a4c8d583133f Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 2 Feb 2022 13:36:18 -0600 Subject: [PATCH 07/15] edited backtesting._get_sell_trade_entry TODO: removed "Other fees" --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 68d71bc58..aa3fa4a8a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -501,7 +501,7 @@ class Backtesting: sell_candle_time: datetime = sell_row[DATE_IDX].to_pydatetime() if self.trading_mode == TradingMode.FUTURES: - # TODO-lev: Other fees / liquidation price? + # TODO-lev: liquidation price? trade.funding_fees = self.exchange.calculate_funding_fees( self.futures_data[trade.pair], amount=trade.amount, From d5376c2c891fa7123fc1e8d113d795de059d70df Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 2 Feb 2022 14:09:49 -0600 Subject: [PATCH 08/15] wrote freqtradebot.test_leverage_prep --- freqtrade/freqtradebot.py | 2 +- tests/test_freqtradebot.py | 122 ++++++++++++++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 91468ee97..6cfb5a489 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -601,7 +601,7 @@ class FreqtradeBot(LoggingMixin): self, pair: str, open_rate: float, - amount: float, + amount: float, # quote currency, includes leverage leverage: float, is_short: bool ) -> Tuple[float, Optional[float]]: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 32b7d543b..91e941e2c 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4782,9 +4782,125 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None: assert valid_price_at_min_alwd < proposed_price -def test_leverage_prep(): - # TODO-lev - return +@pytest.mark.parametrize( + "is_short,trading_mode,exchange_name,margin_mode,leverage,open_rate,amount,expected_liq", [ + (False, 'spot', 'binance', '', 5.0, 10.0, 1.0, None), + (True, 'spot', 'binance', '', 5.0, 10.0, 1.0, None), + (False, 'spot', 'gateio', '', 5.0, 10.0, 1.0, None), + (True, 'spot', 'gateio', '', 5.0, 10.0, 1.0, None), + (False, 'spot', 'okex', '', 5.0, 10.0, 1.0, None), + (True, 'spot', 'okex', '', 5.0, 10.0, 1.0, None), + # Binance, short + (True, 'futures', 'binance', 'isolated', 5.0, 10.0, 1.0, 11.89108910891089), + (True, 'futures', 'binance', 'isolated', 3.0, 10.0, 1.0, 13.211221122079207), + (True, 'futures', 'binance', 'isolated', 5.0, 8.0, 1.0, 9.514851485148514), + (True, 'futures', 'binance', 'isolated', 5.0, 10.0, 0.6, 12.557755775577558), + # Binance, long + (False, 'futures', 'binance', 'isolated', 5, 10, 1.0, 8.070707070707071), + (False, 'futures', 'binance', 'isolated', 5, 8, 1.0, 6.454545454545454), + (False, 'futures', 'binance', 'isolated', 3, 10, 1.0, 6.717171717171718), + (False, 'futures', 'binance', 'isolated', 5, 10, 0.6, 7.39057239057239), + # Gateio/okex, short + (True, 'futures', 'gateio', 'isolated', 5, 10, 1.0, 11.87413417771621), + (True, 'futures', 'gateio', 'isolated', 5, 10, 2.0, 11.87413417771621), + (True, 'futures', 'gateio', 'isolated', 3, 10, 1.0, 13.476180850346978), + (True, 'futures', 'gateio', 'isolated', 5, 8, 1.0, 9.499307342172967), + # Gateio/okex, long + (False, 'futures', 'gateio', 'isolated', 5.0, 10.0, 1.0, 8.085708510208207), + (False, 'futures', 'gateio', 'isolated', 3.0, 10.0, 1.0, 6.738090425173506), + # (True, 'futures', 'okex', 'isolated', 11.87413417771621), + # (False, 'futures', 'okex', 'isolated', 8.085708510208207), + ] +) +def test_leverage_prep( + mocker, + default_conf_usdt, + is_short, + trading_mode, + exchange_name, + margin_mode, + leverage, + open_rate, + amount, + expected_liq, +): + """ + position = 0.2 * 5 + wb: wallet balance (stake_amount if isolated) + cum_b: maintenance amount + side_1: -1 if is_short else 1 + ep1: entry price + mmr_b: maintenance margin ratio + + Binance, Short + leverage = 5, open_rate = 10, amount = 1.0 + ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) + ((2 + 0.01) - ((-1) * 1 * 10)) / ((1 * 0.01) - ((-1) * 1)) = 11.89108910891089 + leverage = 3, open_rate = 10, amount = 1.0 + ((3.3333333333 + 0.01) - ((-1) * 1.0 * 10)) / ((1.0 * 0.01) - ((-1) * 1.0)) = 13.2112211220 + leverage = 5, open_rate = 8, amount = 1.0 + ((1.6 + 0.01) - ((-1) * 1 * 8)) / ((1 * 0.01) - ((-1) * 1)) = 9.514851485148514 + leverage = 5, open_rate = 10, amount = 0.6 + ((1.6 + 0.01) - ((-1) * 0.6 * 10)) / ((0.6 * 0.01) - ((-1) * 0.6)) = 12.557755775577558 + + Binance, Long + leverage = 5, open_rate = 10, amount = 1.0 + ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) + ((2 + 0.01) - (1 * 1 * 10)) / ((1 * 0.01) - (1 * 1)) = 8.070707070707071 + leverage = 5, open_rate = 8, amount = 1.0 + ((1.6 + 0.01) - (1 * 1 * 8)) / ((1 * 0.01) - (1 * 1)) = 6.454545454545454 + leverage = 3, open_rate = 10, amount = 1.0 + ((2 + 0.01) - (1 * 0.6 * 10)) / ((0.6 * 0.01) - (1 * 0.6)) = 6.717171717171718 + leverage = 5, open_rate = 10, amount = 0.6 + ((1.6 + 0.01) - (1 * 0.6 * 10)) / ((0.6 * 0.01) - (1 * 0.6)) = 7.39057239057239 + + Gateio/Okex, Short + leverage = 5, open_rate = 10, amount = 1.0 + (open_rate + (wallet_balance / position)) / (1 + (mm_ratio + taker_fee_rate)) + (10 + (2 / 1.0)) / (1 + (0.01 + 0.0006)) = 11.87413417771621 + leverage = 5, open_rate = 10, amount = 2.0 + (10 + (4 / 2.0)) / (1 + (0.01 + 0.0006)) = 11.87413417771621 + leverage = 3, open_rate = 10, amount = 1.0 + (10 + (3.3333333333333 / 1.0)) / (1 - (0.01 + 0.0006)) = 13.476180850346978 + leverage = 5, open_rate = 8, amount = 1.0 + (8 + (1.6 / 1.0)) / (1 + (0.01 + 0.0006)) = 9.499307342172967 + + Gateio/Okex, Long + leverage = 5, open_rate = 10, amount = 1.0 + (open_rate - (wallet_balance / position)) / (1 - (mm_ratio + taker_fee_rate)) + (10 - (2 / 1)) / (1 - (0.01 + 0.0006)) = 8.085708510208207 + leverage = 5, open_rate = 10, amount = 2.0 + (10 - (4 / 2.0)) / (1 + (0.01 + 0.0006)) = 7.916089451810806 + leverage = 3, open_rate = 10, amount = 1.0 + (10 - (3.333333333333333333 / 1.0)) / (1 - (0.01 + 0.0006)) = 6.738090425173506 + leverage = 5, open_rate = 8, amount = 1.0 + (8 - (1.6 / 1.0)) / (1 + (0.01 + 0.0006)) = 6.332871561448645 + """ + default_conf_usdt['trading_mode'] = trading_mode + default_conf_usdt['exchange']['name'] = exchange_name + default_conf_usdt['margin_mode'] = margin_mode + mocker.patch('freqtrade.exchange.Gateio.validate_ordertypes') + patch_RPCManager(mocker) + patch_exchange(mocker, id=exchange_name) + freqtrade = FreqtradeBot(default_conf_usdt) + + freqtrade.exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01)) + freqtrade.exchange.name = exchange_name + # default_conf_usdt.update({ + # "dry_run": False, + # }) + (interest, liq) = freqtrade.leverage_prep( + pair='ETH/USDT:USDT', + open_rate=open_rate, + amount=amount, + leverage=leverage, + is_short=is_short, + ) + assert interest == 0.0 + if expected_liq is None: + assert liq is None + else: + isclose(expected_liq, liq) @pytest.mark.parametrize('trading_mode,calls,t1,t2', [ From a31cf236e4ce99a2160688df5f2d4db0b213a781 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 2 Feb 2022 15:45:01 -0600 Subject: [PATCH 09/15] freqtradebot._safe_exit_amount, no _safe_exit_amount is needed for futures --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6cfb5a489..210761a3f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1340,13 +1340,13 @@ class FreqtradeBot(LoggingMixin): :return: amount to sell :raise: DependencyException: if available balance is not within 2% of the available amount. """ - # TODO-lev Maybe update? # Update wallets to ensure amounts tied up in a stoploss is now free! self.wallets.update() trade_base_currency = self.exchange.get_pair_base_currency(pair) wallet_amount = self.wallets.get_free(trade_base_currency) logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}") - if wallet_amount >= amount: + if wallet_amount >= amount or self.trading_mode is TradingMode.FUTURES: + # A safe exit amount isn't needed for futures, you can just exit/close the position return amount elif wallet_amount > amount * 0.98: logger.info(f"{pair} - Falling back to wallet-amount {wallet_amount} -> {amount}.") From 1f74cfe841d1eede5a4670104e572eb91edf14b4 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 2 Feb 2022 16:02:54 -0600 Subject: [PATCH 10/15] plot.generate_candlestick_graph Added short equivelent, separating plotting scatter creation to a function --- freqtrade/plot/plotting.py | 78 +++++++++++++++++++------------------- tests/test_plotting.py | 22 ++++++----- 2 files changed, 52 insertions(+), 48 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 3a5ff4311..0fe279e19 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional import pandas as pd @@ -385,6 +385,35 @@ def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots: return fig +def create_scatter( + data, + column_name, + color, + direction +) -> Optional[go.Scatter]: + + if column_name in data.columns: + df_short = data[data[column_name] == 1] + if len(df_short) > 0: + shorts = go.Scatter( + x=df_short.date, + y=df_short.close, + mode='markers', + name=column_name, + marker=dict( + symbol=f"triangle-{direction}-dot", + size=9, + line=dict(width=1), + color=color, + ) + ) + return shorts + else: + logger.warning(f"No {column_name}-signals found.") + + return None + + def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *, indicators1: List[str] = [], indicators2: List[str] = [], @@ -431,44 +460,15 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra ) fig.add_trace(candles, 1, 1) - # TODO-lev: Needs short equivalent - if 'enter_long' in data.columns: - df_buy = data[data['enter_long'] == 1] - if len(df_buy) > 0: - buys = go.Scatter( - x=df_buy.date, - y=df_buy.close, - mode='markers', - name='buy', - marker=dict( - symbol='triangle-up-dot', - size=9, - line=dict(width=1), - color='green', - ) - ) - fig.add_trace(buys, 1, 1) - else: - logger.warning("No buy-signals found.") + longs = create_scatter(data, 'enter_long', 'green', 'up') + exit_longs = create_scatter(data, 'exit_long', 'red', 'down') + shorts = create_scatter(data, 'enter_short', 'blue', 'down') + exit_shorts = create_scatter(data, 'exit_short', 'violet', 'up') + + for scatter in [longs, exit_longs, shorts, exit_shorts]: + if scatter: + fig.add_trace(scatter, 1, 1) - if 'exit_long' in data.columns: - df_sell = data[data['exit_long'] == 1] - if len(df_sell) > 0: - sells = go.Scatter( - x=df_sell.date, - y=df_sell.close, - mode='markers', - name='sell', - marker=dict( - symbol='triangle-down-dot', - size=9, - line=dict(width=1), - color='red', - ) - ) - fig.add_trace(sells, 1, 1) - else: - logger.warning("No sell-signals found.") # Add Bollinger Bands fig = plot_area(fig, 1, data, 'bb_lowerband', 'bb_upperband', label="Bollinger Band") @@ -537,7 +537,7 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], "Profit per pair", "Parallelism", "Underwater", - ]) + ]) fig['layout'].update(title="Freqtrade Profit plot") fig['layout']['yaxis1'].update(title='Price') fig['layout']['yaxis2'].update(title=f'Profit {stake_currency}') diff --git a/tests/test_plotting.py b/tests/test_plotting.py index b14f83bf9..940639465 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -202,6 +202,8 @@ def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, t datadir=testdatadir, timerange=timerange) data['enter_long'] = 0 data['exit_long'] = 0 + data['enter_short'] = 0 + data['exit_short'] = 0 indicators1 = [] indicators2 = [] @@ -222,8 +224,10 @@ def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, t assert row_mock.call_count == 2 assert trades_mock.call_count == 1 - assert log_has("No buy-signals found.", caplog) - assert log_has("No sell-signals found.", caplog) + assert log_has("No enter_long-signals found.", caplog) + assert log_has("No exit_long-signals found.", caplog) + assert log_has("No enter_short-signals found.", caplog) + assert log_has("No exit_short-signals found.", caplog) def test_generate_candlestick_graph_no_trades(default_conf, mocker, testdatadir): @@ -249,7 +253,7 @@ def test_generate_candlestick_graph_no_trades(default_conf, mocker, testdatadir) assert fig.layout.title.text == pair figure = fig.layout.figure - assert len(figure.data) == 6 + assert len(figure.data) == 8 # Candlesticks are plotted first candles = find_trace_in_fig_data(figure.data, "Price") assert isinstance(candles, go.Candlestick) @@ -257,15 +261,15 @@ def test_generate_candlestick_graph_no_trades(default_conf, mocker, testdatadir) volume = find_trace_in_fig_data(figure.data, "Volume") assert isinstance(volume, go.Bar) - buy = find_trace_in_fig_data(figure.data, "buy") - assert isinstance(buy, go.Scatter) + enter_long = find_trace_in_fig_data(figure.data, "enter_long") + assert isinstance(enter_long, go.Scatter) # All buy-signals should be plotted - assert int(data['enter_long'].sum()) == len(buy.x) + assert int(data['enter_long'].sum()) == len(enter_long.x) - sell = find_trace_in_fig_data(figure.data, "sell") - assert isinstance(sell, go.Scatter) + exit_long = find_trace_in_fig_data(figure.data, "exit_long") + assert isinstance(exit_long, go.Scatter) # All buy-signals should be plotted - assert int(data['exit_long'].sum()) == len(sell.x) + assert int(data['exit_long'].sum()) == len(exit_long.x) assert find_trace_in_fig_data(figure.data, "Bollinger Band") From edc0e9c75fe84c8055390439818d8a2f16438d21 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 3 Feb 2022 01:30:33 -0600 Subject: [PATCH 11/15] backtesting._get_ohlcv_as_lists changed candle_type to candle_type_def --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index aa3fa4a8a..08b84f376 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -337,7 +337,7 @@ class Backtesting: pair, self.timeframe, df_analyzed, - CandleType.FUTURES if self.trading_mode == TradingMode.FUTURES else CandleType.SPOT + self.config['candle_type_def'] ) df_analyzed = df_analyzed.drop(df_analyzed.head(1).index) From 84dea0339b1578cd1911bae9a44702a3fdb29712 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 3 Feb 2022 03:57:33 -0600 Subject: [PATCH 12/15] Added todo to freqtradebot._safe_exit_amount --- freqtrade/freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 210761a3f..686737036 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1345,7 +1345,8 @@ class FreqtradeBot(LoggingMixin): trade_base_currency = self.exchange.get_pair_base_currency(pair) wallet_amount = self.wallets.get_free(trade_base_currency) logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}") - if wallet_amount >= amount or self.trading_mode is TradingMode.FUTURES: + # TODO-lev: Get wallet amount + value of positions + if wallet_amount >= amount or self.trading_mode == TradingMode.FUTURES: # A safe exit amount isn't needed for futures, you can just exit/close the position return amount elif wallet_amount > amount * 0.98: From 99b8a8ca79547ae03d31f83130e911cabb34c71b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 3 Feb 2022 17:34:38 -0600 Subject: [PATCH 13/15] Revert "plot.generate_candlestick_graph Added short equivelent, separating plotting scatter creation to a function" This reverts commit 0abba7f9b7299ba3c45df6a2ba6e35ad6a19c5a0. --- freqtrade/plot/plotting.py | 78 +++++++++++++++++++------------------- tests/test_plotting.py | 22 +++++------ 2 files changed, 48 insertions(+), 52 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 0fe279e19..3a5ff4311 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List import pandas as pd @@ -385,35 +385,6 @@ def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots: return fig -def create_scatter( - data, - column_name, - color, - direction -) -> Optional[go.Scatter]: - - if column_name in data.columns: - df_short = data[data[column_name] == 1] - if len(df_short) > 0: - shorts = go.Scatter( - x=df_short.date, - y=df_short.close, - mode='markers', - name=column_name, - marker=dict( - symbol=f"triangle-{direction}-dot", - size=9, - line=dict(width=1), - color=color, - ) - ) - return shorts - else: - logger.warning(f"No {column_name}-signals found.") - - return None - - def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *, indicators1: List[str] = [], indicators2: List[str] = [], @@ -460,15 +431,44 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra ) fig.add_trace(candles, 1, 1) - longs = create_scatter(data, 'enter_long', 'green', 'up') - exit_longs = create_scatter(data, 'exit_long', 'red', 'down') - shorts = create_scatter(data, 'enter_short', 'blue', 'down') - exit_shorts = create_scatter(data, 'exit_short', 'violet', 'up') - - for scatter in [longs, exit_longs, shorts, exit_shorts]: - if scatter: - fig.add_trace(scatter, 1, 1) + # TODO-lev: Needs short equivalent + if 'enter_long' in data.columns: + df_buy = data[data['enter_long'] == 1] + if len(df_buy) > 0: + buys = go.Scatter( + x=df_buy.date, + y=df_buy.close, + mode='markers', + name='buy', + marker=dict( + symbol='triangle-up-dot', + size=9, + line=dict(width=1), + color='green', + ) + ) + fig.add_trace(buys, 1, 1) + else: + logger.warning("No buy-signals found.") + if 'exit_long' in data.columns: + df_sell = data[data['exit_long'] == 1] + if len(df_sell) > 0: + sells = go.Scatter( + x=df_sell.date, + y=df_sell.close, + mode='markers', + name='sell', + marker=dict( + symbol='triangle-down-dot', + size=9, + line=dict(width=1), + color='red', + ) + ) + fig.add_trace(sells, 1, 1) + else: + logger.warning("No sell-signals found.") # Add Bollinger Bands fig = plot_area(fig, 1, data, 'bb_lowerband', 'bb_upperband', label="Bollinger Band") @@ -537,7 +537,7 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], "Profit per pair", "Parallelism", "Underwater", - ]) + ]) fig['layout'].update(title="Freqtrade Profit plot") fig['layout']['yaxis1'].update(title='Price') fig['layout']['yaxis2'].update(title=f'Profit {stake_currency}') diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 940639465..b14f83bf9 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -202,8 +202,6 @@ def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, t datadir=testdatadir, timerange=timerange) data['enter_long'] = 0 data['exit_long'] = 0 - data['enter_short'] = 0 - data['exit_short'] = 0 indicators1 = [] indicators2 = [] @@ -224,10 +222,8 @@ def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, t assert row_mock.call_count == 2 assert trades_mock.call_count == 1 - assert log_has("No enter_long-signals found.", caplog) - assert log_has("No exit_long-signals found.", caplog) - assert log_has("No enter_short-signals found.", caplog) - assert log_has("No exit_short-signals found.", caplog) + assert log_has("No buy-signals found.", caplog) + assert log_has("No sell-signals found.", caplog) def test_generate_candlestick_graph_no_trades(default_conf, mocker, testdatadir): @@ -253,7 +249,7 @@ def test_generate_candlestick_graph_no_trades(default_conf, mocker, testdatadir) assert fig.layout.title.text == pair figure = fig.layout.figure - assert len(figure.data) == 8 + assert len(figure.data) == 6 # Candlesticks are plotted first candles = find_trace_in_fig_data(figure.data, "Price") assert isinstance(candles, go.Candlestick) @@ -261,15 +257,15 @@ def test_generate_candlestick_graph_no_trades(default_conf, mocker, testdatadir) volume = find_trace_in_fig_data(figure.data, "Volume") assert isinstance(volume, go.Bar) - enter_long = find_trace_in_fig_data(figure.data, "enter_long") - assert isinstance(enter_long, go.Scatter) + buy = find_trace_in_fig_data(figure.data, "buy") + assert isinstance(buy, go.Scatter) # All buy-signals should be plotted - assert int(data['enter_long'].sum()) == len(enter_long.x) + assert int(data['enter_long'].sum()) == len(buy.x) - exit_long = find_trace_in_fig_data(figure.data, "exit_long") - assert isinstance(exit_long, go.Scatter) + sell = find_trace_in_fig_data(figure.data, "sell") + assert isinstance(sell, go.Scatter) # All buy-signals should be plotted - assert int(data['exit_long'].sum()) == len(exit_long.x) + assert int(data['exit_long'].sum()) == len(sell.x) assert find_trace_in_fig_data(figure.data, "Bollinger Band") From de557f13868cf9f809f546b10f9f38db1c417fc4 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 3 Feb 2022 17:47:26 -0600 Subject: [PATCH 14/15] models.update removed TODO-lev --- freqtrade/persistence/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f14bc9ae5..2fb0f39c9 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -569,7 +569,6 @@ class LocalTrade(): payment = "BUY" if self.is_short else "SELL" # * On margin shorts, you buy a little bit more than the amount (amount + interest) logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.') - # TODO-lev: Is anything else needed here? self.close(safe_value_fallback(order, 'average', 'price')) elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): self.stoploss_order_id = None From e7d71d01c0872badb726ca735cab27a09397e418 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 3 Feb 2022 20:39:47 -0600 Subject: [PATCH 15/15] removed plotting todo --- freqtrade/plot/plotting.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 3a5ff4311..fee84e5ea 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -431,7 +431,6 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra ) fig.add_trace(candles, 1, 1) - # TODO-lev: Needs short equivalent if 'enter_long' in data.columns: df_buy = data[data['enter_long'] == 1] if len(df_buy) > 0: @@ -537,7 +536,7 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], "Profit per pair", "Parallelism", "Underwater", - ]) + ]) fig['layout'].update(title="Freqtrade Profit plot") fig['layout']['yaxis1'].update(title='Price') fig['layout']['yaxis2'].update(title=f'Profit {stake_currency}')