From f67e0bd6dda1e9c001f8793a50a8237b70a88dcc Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 16 Feb 2022 07:12:24 -0600 Subject: [PATCH 01/14] wallet amount for futures --- freqtrade/exchange/exchange.py | 35 +++++++++++++++++++++++++++++++++- freqtrade/freqtradebot.py | 1 - 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 97e5d2f1e..09bf0ea5a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -342,7 +342,7 @@ class Exchange: def get_pair_base_currency(self, pair: str) -> str: """ - Return a pair's quote currency + Return a pair's base currency """ return self.markets.get(pair, {}).get('base', '') @@ -1158,6 +1158,39 @@ class Exchange: balances.pop("total", None) balances.pop("used", None) + if self.trading_mode == TradingMode.FUTURES: + + open_orders_response: List[dict] = self._api.fetch_open_orders() + open_orders: dict = {} + for order in open_orders_response: + symbol: str = order['symbol'] + open_orders[symbol] = order + + positions: List[dict] = self._api.fetch_positions() + for position in positions: + symbol = position['symbol'] + market: dict = self.markets[symbol] + size: float = self._contracts_to_amount(symbol, position['contracts']) + side: str = position['side'] + if size > 0: + + if symbol in open_orders: + order_amount: float = open_orders[symbol]['remaining'] + else: + order_amount = 0 + + if side == 'short': + currency: str = market['quote'] + else: + currency = market['base'] + + if currency in balances: + balances[currency] = { + 'free': size - order_amount, + 'used': order_amount, + 'total': size, + } + return balances except ccxt.DDoSProtection as e: raise DDosProtection(e) from e diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c1ec9ca06..fd7203882 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1338,7 +1338,6 @@ 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}") - # 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 From f336e7fc5b7c338f23ac2b2571d18fc726deb811 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 16 Feb 2022 07:16:21 -0600 Subject: [PATCH 02/14] exchange.get_balances futures shorts taken out --- freqtrade/exchange/exchange.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 09bf0ea5a..4f1202211 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1179,9 +1179,7 @@ class Exchange: else: order_amount = 0 - if side == 'short': - currency: str = market['quote'] - else: + if side == 'long' or side == 'buy': currency = market['base'] if currency in balances: From 9f4f65e45714a5368a6c77530bac904c6b09a9a5 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 16 Feb 2022 07:26:23 -0600 Subject: [PATCH 03/14] exchange.get_balances minor fix --- freqtrade/exchange/exchange.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 4f1202211..6b8a9bd0f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1175,19 +1175,26 @@ class Exchange: if size > 0: if symbol in open_orders: - order_amount: float = open_orders[symbol]['remaining'] + order = open_orders[symbol] + order_amount: float = order['remaining'] + order_side: str = order['side'] + if order_side == 'buy' or order_side == 'long': + order_amount = 0 else: order_amount = 0 if side == 'long' or side == 'buy': currency = market['base'] + free = size - order_amount - if currency in balances: - balances[currency] = { - 'free': size - order_amount, - 'used': order_amount, - 'total': size, - } + balances[currency] = { + 'free': free, + 'used': order_amount, + 'total': size, + } + balances['free'][currency] = free + balances['used'][currency] = order_amount + balances['total'][currency] = size return balances except ccxt.DDoSProtection as e: From ed65692257e60c4a71b52058f9947df8c3fae2c7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Feb 2022 07:00:39 +0100 Subject: [PATCH 04/14] add get_position exchange wrapper --- freqtrade/exchange/exchange.py | 54 ++++++++++------------------------ freqtrade/wallets.py | 13 ++++++++ 2 files changed, 29 insertions(+), 38 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 6b8a9bd0f..dd664af1b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1158,44 +1158,6 @@ class Exchange: balances.pop("total", None) balances.pop("used", None) - if self.trading_mode == TradingMode.FUTURES: - - open_orders_response: List[dict] = self._api.fetch_open_orders() - open_orders: dict = {} - for order in open_orders_response: - symbol: str = order['symbol'] - open_orders[symbol] = order - - positions: List[dict] = self._api.fetch_positions() - for position in positions: - symbol = position['symbol'] - market: dict = self.markets[symbol] - size: float = self._contracts_to_amount(symbol, position['contracts']) - side: str = position['side'] - if size > 0: - - if symbol in open_orders: - order = open_orders[symbol] - order_amount: float = order['remaining'] - order_side: str = order['side'] - if order_side == 'buy' or order_side == 'long': - order_amount = 0 - else: - order_amount = 0 - - if side == 'long' or side == 'buy': - currency = market['base'] - free = size - order_amount - - balances[currency] = { - 'free': free, - 'used': order_amount, - 'total': size, - } - balances['free'][currency] = free - balances['used'][currency] = order_amount - balances['total'][currency] = size - return balances except ccxt.DDoSProtection as e: raise DDosProtection(e) from e @@ -1205,6 +1167,22 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + @retrier + def get_positions(self) -> List[Dict]: + if self._config['dry_run'] or self.trading_mode != TradingMode.FUTURES: + return [] + try: + positions: List[Dict] = self._api.fetch_positions() + self._log_exchange_response('fetch_positions', positions) + return positions + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get positions due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + @retrier def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict: """ diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 9659c63d9..2124e004e 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -23,6 +23,7 @@ class Wallet(NamedTuple): free: float = 0 used: float = 0 total: float = 0 + position: float = 0 class Wallets: @@ -108,6 +109,18 @@ class Wallets: for currency in deepcopy(self._wallets): if currency not in balances: del self._wallets[currency] + # TODO-lev: Implement dry-run/backtest counterpart + positions = self._exchange.get_positions() + for position in positions: + symbol = position['symbol'] + if position['side'] is None: + # Position is not open ... + continue + size = self._exchange._contracts_to_amount(symbol, position['contracts']) + + self._wallets[symbol] = Wallet( + symbol, position=size + ) def update(self, require_update: bool = True) -> None: """ From e54e6a7295dfcc322daebbb27bf1d71c538ff387 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Feb 2022 10:58:17 +0100 Subject: [PATCH 05/14] Update wallets to also keep Positions --- freqtrade/rpc/rpc.py | 37 +++++++++++++++++++++++++++++++++---- freqtrade/wallets.py | 28 ++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 5ece392f2..1c0c32846 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -30,6 +30,7 @@ from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.strategy.interface import SellCheckTuple +from freqtrade.wallets import PositionWallet, Wallet logger = logging.getLogger(__name__) @@ -566,7 +567,8 @@ class RPC: def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: """ Returns current account balance per crypto """ - output = [] + currencies = [] + positions = [] total = 0.0 try: tickers = self._freqtrade.exchange.get_tickers(cached=True) @@ -577,7 +579,8 @@ class RPC: starting_capital = self._freqtrade.wallets.get_starting_balance() starting_cap_fiat = self._fiat_converter.convert_amount( starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0 - + coin: str + balance: Wallet for coin, balance in self._freqtrade.wallets.get_all_balances().items(): if not balance.total: continue @@ -598,14 +601,39 @@ class RPC: logger.warning(f" Could not get rate for pair {coin}.") continue total = total + (est_stake or 0) - output.append({ + currencies.append({ 'currency': coin, + # TODO: The below can be simplified if we don't assign None to values. 'free': balance.free if balance.free is not None else 0, 'balance': balance.total if balance.total is not None else 0, 'used': balance.used if balance.used is not None else 0, 'est_stake': est_stake or 0, 'stake': stake_currency, }) + symbol: str + position: PositionWallet + for symbol, position in self._freqtrade.wallets.get_all_positions().items(): + + currencies.append({ + 'currency': symbol, + 'free': 0, + 'balance': position.position, + 'used': 0, + 'est_stake': position.collateral, + 'stake': stake_currency, + }) + + positions.append({ + 'currency': symbol, + # 'free': balance.free if balance.free is not None else 0, + # 'balance': balance.total if balance.total is not None else 0, + # 'used': balance.used if balance.used is not None else 0, + 'position': position.position, + 'side': position.side, + 'est_stake': position.collateral, + 'leverage': position.leverage, + 'stake': stake_currency, + }) value = self._fiat_converter.convert_amount( total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 @@ -616,7 +644,8 @@ class RPC: starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0 return { - 'currencies': output, + 'currencies': currencies, + 'positions': positions, 'total': total, 'symbol': fiat_display_currency, 'value': value, diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 2124e004e..f7ee95b0e 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -3,7 +3,7 @@ import logging from copy import deepcopy -from typing import Any, Dict, NamedTuple, Optional +from typing import Dict, NamedTuple, Optional import arrow @@ -26,6 +26,14 @@ class Wallet(NamedTuple): position: float = 0 +class PositionWallet(NamedTuple): + symbol: str + position: float = 0 + leverage: float = 0 + collateral: float = 0 + side: str = 'long' + + class Wallets: def __init__(self, config: dict, exchange: Exchange, log: bool = True) -> None: @@ -33,6 +41,7 @@ class Wallets: self._log = log self._exchange = exchange self._wallets: Dict[str, Wallet] = {} + self._positions: Dict[str, PositionWallet] = {} self.start_cap = config['dry_run_wallet'] self._last_wallet_refresh = 0 self.update() @@ -113,13 +122,17 @@ class Wallets: positions = self._exchange.get_positions() for position in positions: symbol = position['symbol'] - if position['side'] is None: + if position['side'] is None or position['collateral'] == 0.0: # Position is not open ... continue size = self._exchange._contracts_to_amount(symbol, position['contracts']) - - self._wallets[symbol] = Wallet( - symbol, position=size + collateral = position['collateral'] + leverage = position['leverage'] + self._positions[symbol] = PositionWallet( + symbol, position=size, + leverage=leverage, + collateral=collateral, + side=position['side'] ) def update(self, require_update: bool = True) -> None: @@ -139,9 +152,12 @@ class Wallets: logger.info('Wallets synced.') self._last_wallet_refresh = arrow.utcnow().int_timestamp - def get_all_balances(self) -> Dict[str, Any]: + def get_all_balances(self) -> Dict[str, Wallet]: return self._wallets + def get_all_positions(self) -> Dict[str, PositionWallet]: + return self._positions + def get_starting_balance(self) -> float: """ Retrieves starting balance - based on either available capital, From 4b27bd9838fc803b93e6e1695687ce9338c818fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Feb 2022 11:06:47 +0100 Subject: [PATCH 06/14] don't fetch free balance if we don't use it --- freqtrade/freqtradebot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fd7203882..abd38859b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1335,10 +1335,13 @@ class FreqtradeBot(LoggingMixin): """ # Update wallets to ensure amounts tied up in a stoploss is now free! self.wallets.update() + if self.trading_mode == TradingMode.FUTURES: + return amount + 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 == TradingMode.FUTURES: + if wallet_amount >= amount: # 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 a2b17882e697e57e66f532a4cd791c1c017ef35c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Feb 2022 16:28:51 +0100 Subject: [PATCH 07/14] Don't use separate position field in /currency endpoint --- freqtrade/rpc/api_server/api_schemas.py | 5 +++++ freqtrade/rpc/rpc.py | 21 ++++++++------------- freqtrade/rpc/telegram.py | 22 +++++++++++++++------- freqtrade/wallets.py | 2 ++ 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index a1910b4d3..e80bf3eb8 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -39,6 +39,11 @@ class Balance(BaseModel): used: float est_stake: float stake: str + # Starting with 2.x + side: str + leverage: float + is_position: bool + position: float class Balances(BaseModel): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 1c0c32846..d151f8aab 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -609,6 +609,10 @@ class RPC: 'used': balance.used if balance.used is not None else 0, 'est_stake': est_stake or 0, 'stake': stake_currency, + 'side': 'long', + 'leverage': 1, + 'position': 0, + 'is_position': False, }) symbol: str position: PositionWallet @@ -617,22 +621,14 @@ class RPC: currencies.append({ 'currency': symbol, 'free': 0, - 'balance': position.position, + 'balance': 0, 'used': 0, - 'est_stake': position.collateral, - 'stake': stake_currency, - }) - - positions.append({ - 'currency': symbol, - # 'free': balance.free if balance.free is not None else 0, - # 'balance': balance.total if balance.total is not None else 0, - # 'used': balance.used if balance.used is not None else 0, 'position': position.position, - 'side': position.side, 'est_stake': position.collateral, - 'leverage': position.leverage, 'stake': stake_currency, + 'leverage': position.leverage, + 'side': position.side, + 'is_position': True }) value = self._fiat_converter.convert_amount( @@ -645,7 +641,6 @@ class RPC: return { 'currencies': currencies, - 'positions': positions, 'total': total, 'symbol': fiat_display_currency, 'value': value, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index bfc56d2c7..f11003d52 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -827,13 +827,21 @@ class Telegram(RPCHandler): for curr in result['currencies']: curr_output = '' if curr['est_stake'] > balance_dust_level: - curr_output = ( - f"*{curr['currency']}:*\n" - f"\t`Available: {curr['free']:.8f}`\n" - f"\t`Balance: {curr['balance']:.8f}`\n" - f"\t`Pending: {curr['used']:.8f}`\n" - f"\t`Est. {curr['stake']}: " - f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") + if curr['is_position']: + curr_output = ( + f"*{curr['currency']}:*\n" + f"\t`{curr['side']}: {curr['position']:.8f}`\n" + f"\t`Leverage: {curr['leverage']:.1f}`\n" + f"\t`Est. {curr['stake']}: " + f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") + else: + curr_output = ( + f"*{curr['currency']}:*\n" + f"\t`Available: {curr['free']:.8f}`\n" + f"\t`Balance: {curr['balance']:.8f}`\n" + f"\t`Pending: {curr['used']:.8f}`\n" + f"\t`Est. {curr['stake']}: " + f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") elif curr['est_stake'] <= balance_dust_level: total_dust_balance += curr['est_stake'] total_dust_currencies += 1 diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index f7ee95b0e..a04602075 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -118,8 +118,10 @@ class Wallets: for currency in deepcopy(self._wallets): if currency not in balances: del self._wallets[currency] + # TODO-lev: Implement dry-run/backtest counterpart positions = self._exchange.get_positions() + self._positions = [] for position in positions: symbol = position['symbol'] if position['side'] is None or position['collateral'] == 0.0: From 656251113701af877fab92a2b22815727f5f768a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Feb 2022 07:17:12 +0100 Subject: [PATCH 08/14] add trade_direction to trade object --- freqtrade/persistence/models.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f7317a7d5..18491d687 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -385,6 +385,13 @@ class LocalTrade(): else: return "sell" + @property + def trade_direction(self) -> str: + if self.is_short: + return "short" + else: + return "long" + def __init__(self, **kwargs): for key in kwargs: setattr(self, key, kwargs[key]) From 13e74c5693e68ddb6b7afa4559ac23d2ec8ee26c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Feb 2022 19:14:46 +0100 Subject: [PATCH 09/14] Add dry-run position wallet calculation --- freqtrade/wallets.py | 56 ++++++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index a04602075..c1f291731 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -8,7 +8,7 @@ from typing import Dict, NamedTuple, Optional import arrow from freqtrade.constants import UNLIMITED_STAKE_AMOUNT -from freqtrade.enums import RunMode +from freqtrade.enums import RunMode, TradingMode from freqtrade.exceptions import DependencyException from freqtrade.exchange import Exchange from freqtrade.persistence import LocalTrade, Trade @@ -23,7 +23,6 @@ class Wallet(NamedTuple): free: float = 0 used: float = 0 total: float = 0 - position: float = 0 class PositionWallet(NamedTuple): @@ -76,6 +75,7 @@ class Wallets: """ # Recreate _wallets to reset closed trade balances _wallets = {} + _positions = {} open_trades = Trade.get_trades_proxy(is_open=True) # If not backtesting... # TODO: potentially remove the ._log workaround to determine backtest mode. @@ -84,24 +84,46 @@ class Wallets: else: tot_profit = LocalTrade.total_profit tot_in_trades = sum(trade.stake_amount for trade in open_trades) + used_stake = 0.0 + + if self._config.get('trading_mode', 'spot') != TradingMode.FUTURES: + current_stake = self.start_cap + tot_profit - tot_in_trades + total_stake = current_stake + for trade in open_trades: + curr = self._exchange.get_pair_base_currency(trade.pair) + _wallets[curr] = Wallet( + curr, + trade.amount, + 0, + trade.amount + ) + else: + tot_in_trades = 0 + for position in open_trades: + # size = self._exchange._contracts_to_amount(position.pair, position['contracts']) + size = position.amount + # TODO-lev: stake_amount in real trades does not include the leverage ... + collateral = position.stake_amount / position.leverage + leverage = position.leverage + tot_in_trades -= collateral + _positions[position.pair] = PositionWallet( + position.pair, position=size, + leverage=leverage, + collateral=collateral, + side=position.trade_direction + ) + current_stake = self.start_cap + tot_profit + used_stake = tot_in_trades + total_stake = current_stake - tot_in_trades - current_stake = self.start_cap + tot_profit - tot_in_trades _wallets[self._config['stake_currency']] = Wallet( - self._config['stake_currency'], - current_stake, - 0, - current_stake + currency=self._config['stake_currency'], + free=current_stake, + used=used_stake, + total=total_stake ) - - for trade in open_trades: - curr = self._exchange.get_pair_base_currency(trade.pair) - _wallets[curr] = Wallet( - curr, - trade.amount, - 0, - trade.amount - ) self._wallets = _wallets + self._positions = _positions def _update_live(self) -> None: balances = self._exchange.get_balances() @@ -121,7 +143,7 @@ class Wallets: # TODO-lev: Implement dry-run/backtest counterpart positions = self._exchange.get_positions() - self._positions = [] + self._positions = {} for position in positions: symbol = position['symbol'] if position['side'] is None or position['collateral'] == 0.0: From d07a24a54fab736ae31fc2a9033fa6238a8c6c14 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Feb 2022 19:30:07 +0100 Subject: [PATCH 10/14] Update tests for new wallet RPC structure --- freqtrade/rpc/rpc.py | 1 - tests/rpc/test_rpc.py | 12 ++++++++++++ tests/rpc/test_rpc_apiserver.py | 4 ++++ tests/rpc/test_rpc_telegram.py | 4 ++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d151f8aab..9b780d88d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -568,7 +568,6 @@ class RPC: def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: """ Returns current account balance per crypto """ currencies = [] - positions = [] total = 0.0 try: tickers = self._freqtrade.exchange.get_tickers(cached=True) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 0dd8b3b93..6a02d6489 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -636,6 +636,10 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'used': 2.0, 'est_stake': 12.0, 'stake': 'BTC', + 'is_position': False, + 'leverage': 1.0, + 'position': 0.0, + 'side': 'long', }, {'free': 1.0, 'balance': 5.0, @@ -643,6 +647,10 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'est_stake': 0.30794, 'used': 4.0, 'stake': 'BTC', + 'is_position': False, + 'leverage': 1.0, + 'position': 0.0, + 'side': 'long', }, {'free': 5.0, @@ -651,6 +659,10 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'est_stake': 0.0011563153318162476, 'used': 5.0, 'stake': 'BTC', + 'is_position': False, + 'leverage': 1.0, + 'position': 0.0, + 'side': 'long', } ] assert result['total'] == 12.309096315331816 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index c103f0ef4..76e4ed9f2 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -453,6 +453,10 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers): 'used': 0.0, 'est_stake': 12.0, 'stake': 'BTC', + 'is_position': False, + 'leverage': 1.0, + 'position': 0.0, + 'side': 'long', } assert 'starting_capital' in response assert 'starting_capital_fiat' in response diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index be3983331..ed0c940fe 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -905,6 +905,10 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None 'balance': i, 'est_stake': 1, 'stake': 'BTC', + 'is_position': False, + 'leverage': 1.0, + 'position': 0.0, + 'side': 'long', }) mocker.patch('freqtrade.rpc.rpc.RPC._rpc_balance', return_value={ 'currencies': balances, From 62c42a73e2cb9d0cdc2375e836cc884da15a8637 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Feb 2022 07:40:15 +0100 Subject: [PATCH 11/14] Add initial rpc test --- freqtrade/wallets.py | 1 - tests/rpc/test_rpc.py | 108 ++++++++++++++++++++++++++------------ tests/test_persistence.py | 1 + tests/test_wallets.py | 101 +++++++++++++++++++++++++++++++++++ 4 files changed, 177 insertions(+), 34 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index c1f291731..8d8216cf6 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -141,7 +141,6 @@ class Wallets: if currency not in balances: del self._wallets[currency] - # TODO-lev: Implement dry-run/backtest counterpart positions = self._exchange.get_positions() self._positions = {} for position in positions: diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 6a02d6489..09428b8b4 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -603,6 +603,30 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'used': 5.0, } } + mock_pos = [ + { + "symbol": "ETH/USDT:USDT", + "timestamp": None, + "datetime": None, + "initialMargin": 0.0, + "initialMarginPercentage": None, + "maintenanceMargin": 0.0, + "maintenanceMarginPercentage": 0.005, + "entryPrice": 0.0, + "notional": 100.0, + "leverage": 5.0, + "unrealizedPnl": 0.0, + "contracts": 100.0, + "contractSize": 1, + "marginRatio": None, + "liquidationPrice": 0.0, + "markPrice": 2896.41, + "collateral": 20, + "marginType": "isolated", + "side": 'short', + "percentage": None + } + ] mocker.patch.multiple( 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', @@ -612,12 +636,15 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', + validate_trading_mode_and_margin_mode=MagicMock(), get_balances=MagicMock(return_value=mock_balance), + get_positions=MagicMock(return_value=mock_pos), get_tickers=tickers, get_valid_pair_combination=MagicMock( side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}") ) default_conf['dry_run'] = False + default_conf['trading_mode'] = 'futures' freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot) rpc = RPC(freqtradebot) @@ -630,40 +657,55 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): assert tickers.call_args_list[0][1]['cached'] is True assert 'USD' == result['symbol'] assert result['currencies'] == [ - {'currency': 'BTC', - 'free': 10.0, - 'balance': 12.0, - 'used': 2.0, - 'est_stake': 12.0, - 'stake': 'BTC', - 'is_position': False, - 'leverage': 1.0, - 'position': 0.0, - 'side': 'long', - }, - {'free': 1.0, - 'balance': 5.0, - 'currency': 'ETH', - 'est_stake': 0.30794, - 'used': 4.0, - 'stake': 'BTC', - 'is_position': False, - 'leverage': 1.0, - 'position': 0.0, - 'side': 'long', + { + 'currency': 'BTC', + 'free': 10.0, + 'balance': 12.0, + 'used': 2.0, + 'est_stake': 12.0, + 'stake': 'BTC', + 'is_position': False, + 'leverage': 1.0, + 'position': 0.0, + 'side': 'long', + }, + { + 'free': 1.0, + 'balance': 5.0, + 'currency': 'ETH', + 'est_stake': 0.30794, + 'used': 4.0, + 'stake': 'BTC', + 'is_position': False, + 'leverage': 1.0, + 'position': 0.0, + 'side': 'long', - }, - {'free': 5.0, - 'balance': 10.0, - 'currency': 'USDT', - 'est_stake': 0.0011563153318162476, - 'used': 5.0, - 'stake': 'BTC', - 'is_position': False, - 'leverage': 1.0, - 'position': 0.0, - 'side': 'long', - } + }, + { + 'free': 5.0, + 'balance': 10.0, + 'currency': 'USDT', + 'est_stake': 0.0011563153318162476, + 'used': 5.0, + 'stake': 'BTC', + 'is_position': False, + 'leverage': 1.0, + 'position': 0.0, + 'side': 'long', + }, + { + 'free': 0.0, + 'balance': 0.0, + 'currency': 'ETH/USDT:USDT', + 'est_stake': 20, + 'used': 0, + 'stake': 'BTC', + 'is_position': True, + 'leverage': 5.0, + 'position': 1000.0, + 'side': 'short', + } ] assert result['total'] == 12.309096315331816 diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 484c6e8a8..f7273950a 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -95,6 +95,7 @@ def test_enter_exit_side(fee, is_short): ) assert trade.enter_side == enter_side assert trade.exit_side == exit_side + assert trade.trade_direction == 'short' if is_short else 'long' @pytest.mark.usefixtures("init_persistence") diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 4dd1c925f..359d63bca 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -234,3 +234,104 @@ def test_get_starting_balance(mocker, default_conf, available_capital, closed_pr freqtrade = get_patched_freqtradebot(mocker, default_conf) assert freqtrade.wallets.get_starting_balance() == expected + + +def test_sync_wallet_futures_live(mocker, default_conf): + default_conf['dry_run'] = False + default_conf['trading_mode'] = 'futures' + default_conf['margin_mode'] = 'isolated' + mock_result = [ + { + "symbol": "ETH/USDT:USDT", + "timestamp": None, + "datetime": None, + "initialMargin": 0.0, + "initialMarginPercentage": None, + "maintenanceMargin": 0.0, + "maintenanceMarginPercentage": 0.005, + "entryPrice": 0.0, + "notional": 100.0, + "leverage": 5.0, + "unrealizedPnl": 0.0, + "contracts": 100.0, + "contractSize": 1, + "marginRatio": None, + "liquidationPrice": 0.0, + "markPrice": 2896.41, + "collateral": 20, + "marginType": "isolated", + "side": 'short', + "percentage": None + }, + { + "symbol": "ADA/USDT:USDT", + "timestamp": None, + "datetime": None, + "initialMargin": 0.0, + "initialMarginPercentage": None, + "maintenanceMargin": 0.0, + "maintenanceMarginPercentage": 0.005, + "entryPrice": 0.0, + "notional": 100.0, + "leverage": 5.0, + "unrealizedPnl": 0.0, + "contracts": 100.0, + "contractSize": 1, + "marginRatio": None, + "liquidationPrice": 0.0, + "markPrice": 0.91, + "collateral": 20, + "marginType": "isolated", + "side": 'short', + "percentage": None + }, + { + # Closed position + "symbol": "SOL/BUSD:BUSD", + "timestamp": None, + "datetime": None, + "initialMargin": 0.0, + "initialMarginPercentage": None, + "maintenanceMargin": 0.0, + "maintenanceMarginPercentage": 0.005, + "entryPrice": 0.0, + "notional": 0.0, + "leverage": 5.0, + "unrealizedPnl": 0.0, + "contracts": 0.0, + "contractSize": 1, + "marginRatio": None, + "liquidationPrice": 0.0, + "markPrice": 15.41, + "collateral": 0.0, + "marginType": "isolated", + "side": 'short', + "percentage": None + } + ] + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value={ + "USDT": { + "free": 900, + "used": 100, + "total": 1000 + }, + }), + get_positions=MagicMock(return_value=mock_result) + ) + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + assert len(freqtrade.wallets._wallets) == 1 + assert len(freqtrade.wallets._positions) == 2 + + assert 'USDT' in freqtrade.wallets._wallets + assert 'ETH/USDT:USDT' in freqtrade.wallets._positions + assert freqtrade.wallets._last_wallet_refresh > 0 + + # Remove ETH/USDT:USDT position + del mock_result[0] + freqtrade.wallets.update() + assert len(freqtrade.wallets._positions) == 1 + assert 'ETH/USDT:USDT' not in freqtrade.wallets._positions From 9901decf0d2b99ba86048f6f4e7e9eceacf0b1bd Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Feb 2022 19:19:42 +0100 Subject: [PATCH 12/14] Rename get_positions to fetch_positions to align with ccxt naming --- freqtrade/exchange/exchange.py | 2 +- freqtrade/wallets.py | 2 +- tests/rpc/test_rpc.py | 2 +- tests/test_wallets.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index dd664af1b..2703c99bf 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1168,7 +1168,7 @@ class Exchange: raise OperationalException(e) from e @retrier - def get_positions(self) -> List[Dict]: + def fetch_positions(self) -> List[Dict]: if self._config['dry_run'] or self.trading_mode != TradingMode.FUTURES: return [] try: diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 8d8216cf6..153512897 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -141,7 +141,7 @@ class Wallets: if currency not in balances: del self._wallets[currency] - positions = self._exchange.get_positions() + positions = self._exchange.fetch_positions() self._positions = {} for position in positions: symbol = position['symbol'] diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 09428b8b4..9bb809aaa 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -638,7 +638,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'freqtrade.exchange.Exchange', validate_trading_mode_and_margin_mode=MagicMock(), get_balances=MagicMock(return_value=mock_balance), - get_positions=MagicMock(return_value=mock_pos), + fetch_positions=MagicMock(return_value=mock_pos), get_tickers=tickers, get_valid_pair_combination=MagicMock( side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}") diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 359d63bca..a2e0ed48c 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -318,7 +318,7 @@ def test_sync_wallet_futures_live(mocker, default_conf): "total": 1000 }, }), - get_positions=MagicMock(return_value=mock_result) + fetch_positions=MagicMock(return_value=mock_result) ) freqtrade = get_patched_freqtradebot(mocker, default_conf) From 9d55621f42c0e4cafa55c967f2b41d02d44d7886 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Feb 2022 19:27:40 +0100 Subject: [PATCH 13/14] Test fetch_position exchange method --- tests/exchange/test_exchange.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 9c54686e6..894f5b75b 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1547,6 +1547,27 @@ def test_get_balances_prod(default_conf, mocker, exchange_name): "get_balances", "fetch_balance") +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_fetch_positions(default_conf, mocker, exchange_name): + mocker.patch('freqtrade.exchange.Exchange.validate_trading_mode_and_margin_mode') + api_mock = MagicMock() + api_mock.fetch_positions = MagicMock(return_value=[ + {'symbol': 'ETH/USDT:USDT', 'leverage': 5}, + {'symbol': 'XRP/USDT:USDT', 'leverage': 5}, + ]) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + assert exchange.fetch_positions() == [] + default_conf['dry_run'] = False + default_conf['trading_mode'] = 'futures' + + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + res = exchange.fetch_positions() + assert len(res) == 2 + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, + "fetch_positions", "fetch_positions") + + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_tickers(default_conf, mocker, exchange_name): api_mock = MagicMock() From 1c26ff4c4c2a46294a4a249fe956378908e452ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Feb 2022 19:52:25 +0100 Subject: [PATCH 14/14] Add dry run test --- tests/test_wallets.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index a2e0ed48c..e7b804a0b 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -6,7 +6,7 @@ import pytest from freqtrade.constants import UNLIMITED_STAKE_AMOUNT from freqtrade.exceptions import DependencyException -from tests.conftest import get_patched_freqtradebot, patch_wallet +from tests.conftest import create_mock_trades, get_patched_freqtradebot, patch_wallet def test_sync_wallet_at_boot(mocker, default_conf): @@ -335,3 +335,24 @@ def test_sync_wallet_futures_live(mocker, default_conf): freqtrade.wallets.update() assert len(freqtrade.wallets._positions) == 1 assert 'ETH/USDT:USDT' not in freqtrade.wallets._positions + + +def test_sync_wallet_futures_dry(mocker, default_conf, fee): + default_conf['dry_run'] = True + default_conf['trading_mode'] = 'futures' + default_conf['margin_mode'] = 'isolated' + freqtrade = get_patched_freqtradebot(mocker, default_conf) + assert len(freqtrade.wallets._wallets) == 1 + assert len(freqtrade.wallets._positions) == 0 + + create_mock_trades(fee, is_short=None) + + freqtrade.wallets.update() + + assert len(freqtrade.wallets._wallets) == 1 + assert len(freqtrade.wallets._positions) == 4 + positions = freqtrade.wallets.get_all_positions() + positions['ETH/BTC'].side == 'short' + positions['ETC/BTC'].side == 'long' + positions['XRP/BTC'].side == 'long' + positions['LTC/BTC'].side == 'short'