Merge pull request #6423 from samgermain/wallet-amt
Futures wallet amount
This commit is contained in:
commit
79ddc9abaa
@ -342,7 +342,7 @@ class Exchange:
|
|||||||
|
|
||||||
def get_pair_base_currency(self, pair: str) -> str:
|
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', '')
|
return self.markets.get(pair, {}).get('base', '')
|
||||||
|
|
||||||
@ -1167,6 +1167,22 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def fetch_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
|
@retrier
|
||||||
def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict:
|
def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict:
|
||||||
"""
|
"""
|
||||||
|
@ -1335,11 +1335,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
"""
|
"""
|
||||||
# Update wallets to ensure amounts tied up in a stoploss is now free!
|
# Update wallets to ensure amounts tied up in a stoploss is now free!
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
return amount
|
||||||
|
|
||||||
trade_base_currency = self.exchange.get_pair_base_currency(pair)
|
trade_base_currency = self.exchange.get_pair_base_currency(pair)
|
||||||
wallet_amount = self.wallets.get_free(trade_base_currency)
|
wallet_amount = self.wallets.get_free(trade_base_currency)
|
||||||
logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}")
|
logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}")
|
||||||
# TODO-lev: Get wallet amount + value of positions
|
if wallet_amount >= amount:
|
||||||
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
|
# A safe exit amount isn't needed for futures, you can just exit/close the position
|
||||||
return amount
|
return amount
|
||||||
elif wallet_amount > amount * 0.98:
|
elif wallet_amount > amount * 0.98:
|
||||||
|
@ -385,6 +385,13 @@ class LocalTrade():
|
|||||||
else:
|
else:
|
||||||
return "sell"
|
return "sell"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def trade_direction(self) -> str:
|
||||||
|
if self.is_short:
|
||||||
|
return "short"
|
||||||
|
else:
|
||||||
|
return "long"
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
for key in kwargs:
|
for key in kwargs:
|
||||||
setattr(self, key, kwargs[key])
|
setattr(self, key, kwargs[key])
|
||||||
|
@ -39,6 +39,11 @@ class Balance(BaseModel):
|
|||||||
used: float
|
used: float
|
||||||
est_stake: float
|
est_stake: float
|
||||||
stake: str
|
stake: str
|
||||||
|
# Starting with 2.x
|
||||||
|
side: str
|
||||||
|
leverage: float
|
||||||
|
is_position: bool
|
||||||
|
position: float
|
||||||
|
|
||||||
|
|
||||||
class Balances(BaseModel):
|
class Balances(BaseModel):
|
||||||
|
@ -30,6 +30,7 @@ from freqtrade.persistence.models import PairLock
|
|||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||||
from freqtrade.strategy.interface import SellCheckTuple
|
from freqtrade.strategy.interface import SellCheckTuple
|
||||||
|
from freqtrade.wallets import PositionWallet, Wallet
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -566,7 +567,7 @@ class RPC:
|
|||||||
|
|
||||||
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
|
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
|
||||||
""" Returns current account balance per crypto """
|
""" Returns current account balance per crypto """
|
||||||
output = []
|
currencies = []
|
||||||
total = 0.0
|
total = 0.0
|
||||||
try:
|
try:
|
||||||
tickers = self._freqtrade.exchange.get_tickers(cached=True)
|
tickers = self._freqtrade.exchange.get_tickers(cached=True)
|
||||||
@ -577,7 +578,8 @@ class RPC:
|
|||||||
starting_capital = self._freqtrade.wallets.get_starting_balance()
|
starting_capital = self._freqtrade.wallets.get_starting_balance()
|
||||||
starting_cap_fiat = self._fiat_converter.convert_amount(
|
starting_cap_fiat = self._fiat_converter.convert_amount(
|
||||||
starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
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():
|
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
|
||||||
if not balance.total:
|
if not balance.total:
|
||||||
continue
|
continue
|
||||||
@ -598,13 +600,34 @@ class RPC:
|
|||||||
logger.warning(f" Could not get rate for pair {coin}.")
|
logger.warning(f" Could not get rate for pair {coin}.")
|
||||||
continue
|
continue
|
||||||
total = total + (est_stake or 0)
|
total = total + (est_stake or 0)
|
||||||
output.append({
|
currencies.append({
|
||||||
'currency': coin,
|
'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,
|
'free': balance.free if balance.free is not None else 0,
|
||||||
'balance': balance.total if balance.total 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,
|
'used': balance.used if balance.used is not None else 0,
|
||||||
'est_stake': est_stake or 0,
|
'est_stake': est_stake or 0,
|
||||||
'stake': stake_currency,
|
'stake': stake_currency,
|
||||||
|
'side': 'long',
|
||||||
|
'leverage': 1,
|
||||||
|
'position': 0,
|
||||||
|
'is_position': False,
|
||||||
|
})
|
||||||
|
symbol: str
|
||||||
|
position: PositionWallet
|
||||||
|
for symbol, position in self._freqtrade.wallets.get_all_positions().items():
|
||||||
|
|
||||||
|
currencies.append({
|
||||||
|
'currency': symbol,
|
||||||
|
'free': 0,
|
||||||
|
'balance': 0,
|
||||||
|
'used': 0,
|
||||||
|
'position': position.position,
|
||||||
|
'est_stake': position.collateral,
|
||||||
|
'stake': stake_currency,
|
||||||
|
'leverage': position.leverage,
|
||||||
|
'side': position.side,
|
||||||
|
'is_position': True
|
||||||
})
|
})
|
||||||
|
|
||||||
value = self._fiat_converter.convert_amount(
|
value = self._fiat_converter.convert_amount(
|
||||||
@ -616,7 +639,7 @@ class RPC:
|
|||||||
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
|
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'currencies': output,
|
'currencies': currencies,
|
||||||
'total': total,
|
'total': total,
|
||||||
'symbol': fiat_display_currency,
|
'symbol': fiat_display_currency,
|
||||||
'value': value,
|
'value': value,
|
||||||
|
@ -827,13 +827,21 @@ class Telegram(RPCHandler):
|
|||||||
for curr in result['currencies']:
|
for curr in result['currencies']:
|
||||||
curr_output = ''
|
curr_output = ''
|
||||||
if curr['est_stake'] > balance_dust_level:
|
if curr['est_stake'] > balance_dust_level:
|
||||||
curr_output = (
|
if curr['is_position']:
|
||||||
f"*{curr['currency']}:*\n"
|
curr_output = (
|
||||||
f"\t`Available: {curr['free']:.8f}`\n"
|
f"*{curr['currency']}:*\n"
|
||||||
f"\t`Balance: {curr['balance']:.8f}`\n"
|
f"\t`{curr['side']}: {curr['position']:.8f}`\n"
|
||||||
f"\t`Pending: {curr['used']:.8f}`\n"
|
f"\t`Leverage: {curr['leverage']:.1f}`\n"
|
||||||
f"\t`Est. {curr['stake']}: "
|
f"\t`Est. {curr['stake']}: "
|
||||||
f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n")
|
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:
|
elif curr['est_stake'] <= balance_dust_level:
|
||||||
total_dust_balance += curr['est_stake']
|
total_dust_balance += curr['est_stake']
|
||||||
total_dust_currencies += 1
|
total_dust_currencies += 1
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Any, Dict, NamedTuple, Optional
|
from typing import Dict, NamedTuple, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
|
|
||||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
|
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.exceptions import DependencyException
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.persistence import LocalTrade, Trade
|
from freqtrade.persistence import LocalTrade, Trade
|
||||||
@ -25,6 +25,14 @@ class Wallet(NamedTuple):
|
|||||||
total: float = 0
|
total: float = 0
|
||||||
|
|
||||||
|
|
||||||
|
class PositionWallet(NamedTuple):
|
||||||
|
symbol: str
|
||||||
|
position: float = 0
|
||||||
|
leverage: float = 0
|
||||||
|
collateral: float = 0
|
||||||
|
side: str = 'long'
|
||||||
|
|
||||||
|
|
||||||
class Wallets:
|
class Wallets:
|
||||||
|
|
||||||
def __init__(self, config: dict, exchange: Exchange, log: bool = True) -> None:
|
def __init__(self, config: dict, exchange: Exchange, log: bool = True) -> None:
|
||||||
@ -32,6 +40,7 @@ class Wallets:
|
|||||||
self._log = log
|
self._log = log
|
||||||
self._exchange = exchange
|
self._exchange = exchange
|
||||||
self._wallets: Dict[str, Wallet] = {}
|
self._wallets: Dict[str, Wallet] = {}
|
||||||
|
self._positions: Dict[str, PositionWallet] = {}
|
||||||
self.start_cap = config['dry_run_wallet']
|
self.start_cap = config['dry_run_wallet']
|
||||||
self._last_wallet_refresh = 0
|
self._last_wallet_refresh = 0
|
||||||
self.update()
|
self.update()
|
||||||
@ -66,6 +75,7 @@ class Wallets:
|
|||||||
"""
|
"""
|
||||||
# Recreate _wallets to reset closed trade balances
|
# Recreate _wallets to reset closed trade balances
|
||||||
_wallets = {}
|
_wallets = {}
|
||||||
|
_positions = {}
|
||||||
open_trades = Trade.get_trades_proxy(is_open=True)
|
open_trades = Trade.get_trades_proxy(is_open=True)
|
||||||
# If not backtesting...
|
# If not backtesting...
|
||||||
# TODO: potentially remove the ._log workaround to determine backtest mode.
|
# TODO: potentially remove the ._log workaround to determine backtest mode.
|
||||||
@ -74,24 +84,46 @@ class Wallets:
|
|||||||
else:
|
else:
|
||||||
tot_profit = LocalTrade.total_profit
|
tot_profit = LocalTrade.total_profit
|
||||||
tot_in_trades = sum(trade.stake_amount for trade in open_trades)
|
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(
|
_wallets[self._config['stake_currency']] = Wallet(
|
||||||
self._config['stake_currency'],
|
currency=self._config['stake_currency'],
|
||||||
current_stake,
|
free=current_stake,
|
||||||
0,
|
used=used_stake,
|
||||||
current_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._wallets = _wallets
|
||||||
|
self._positions = _positions
|
||||||
|
|
||||||
def _update_live(self) -> None:
|
def _update_live(self) -> None:
|
||||||
balances = self._exchange.get_balances()
|
balances = self._exchange.get_balances()
|
||||||
@ -109,6 +141,23 @@ class Wallets:
|
|||||||
if currency not in balances:
|
if currency not in balances:
|
||||||
del self._wallets[currency]
|
del self._wallets[currency]
|
||||||
|
|
||||||
|
positions = self._exchange.fetch_positions()
|
||||||
|
self._positions = {}
|
||||||
|
for position in positions:
|
||||||
|
symbol = position['symbol']
|
||||||
|
if position['side'] is None or position['collateral'] == 0.0:
|
||||||
|
# Position is not open ...
|
||||||
|
continue
|
||||||
|
size = self._exchange._contracts_to_amount(symbol, position['contracts'])
|
||||||
|
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:
|
def update(self, require_update: bool = True) -> None:
|
||||||
"""
|
"""
|
||||||
Updates wallets from the configured version.
|
Updates wallets from the configured version.
|
||||||
@ -126,9 +175,12 @@ class Wallets:
|
|||||||
logger.info('Wallets synced.')
|
logger.info('Wallets synced.')
|
||||||
self._last_wallet_refresh = arrow.utcnow().int_timestamp
|
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
|
return self._wallets
|
||||||
|
|
||||||
|
def get_all_positions(self) -> Dict[str, PositionWallet]:
|
||||||
|
return self._positions
|
||||||
|
|
||||||
def get_starting_balance(self) -> float:
|
def get_starting_balance(self) -> float:
|
||||||
"""
|
"""
|
||||||
Retrieves starting balance - based on either available capital,
|
Retrieves starting balance - based on either available capital,
|
||||||
|
@ -1547,6 +1547,27 @@ def test_get_balances_prod(default_conf, mocker, exchange_name):
|
|||||||
"get_balances", "fetch_balance")
|
"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)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_get_tickers(default_conf, mocker, exchange_name):
|
def test_get_tickers(default_conf, mocker, exchange_name):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
|
@ -603,6 +603,30 @@ def test_rpc_balance_handle(default_conf, mocker, tickers):
|
|||||||
'used': 5.0,
|
'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(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.fiat_convert.CoinGeckoAPI',
|
'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('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
validate_trading_mode_and_margin_mode=MagicMock(),
|
||||||
get_balances=MagicMock(return_value=mock_balance),
|
get_balances=MagicMock(return_value=mock_balance),
|
||||||
|
fetch_positions=MagicMock(return_value=mock_pos),
|
||||||
get_tickers=tickers,
|
get_tickers=tickers,
|
||||||
get_valid_pair_combination=MagicMock(
|
get_valid_pair_combination=MagicMock(
|
||||||
side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}")
|
side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}")
|
||||||
)
|
)
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
|
default_conf['trading_mode'] = 'futures'
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
@ -630,28 +657,55 @@ def test_rpc_balance_handle(default_conf, mocker, tickers):
|
|||||||
assert tickers.call_args_list[0][1]['cached'] is True
|
assert tickers.call_args_list[0][1]['cached'] is True
|
||||||
assert 'USD' == result['symbol']
|
assert 'USD' == result['symbol']
|
||||||
assert result['currencies'] == [
|
assert result['currencies'] == [
|
||||||
{'currency': 'BTC',
|
{
|
||||||
'free': 10.0,
|
'currency': 'BTC',
|
||||||
'balance': 12.0,
|
'free': 10.0,
|
||||||
'used': 2.0,
|
'balance': 12.0,
|
||||||
'est_stake': 12.0,
|
'used': 2.0,
|
||||||
'stake': 'BTC',
|
'est_stake': 12.0,
|
||||||
},
|
'stake': 'BTC',
|
||||||
{'free': 1.0,
|
'is_position': False,
|
||||||
'balance': 5.0,
|
'leverage': 1.0,
|
||||||
'currency': 'ETH',
|
'position': 0.0,
|
||||||
'est_stake': 0.30794,
|
'side': 'long',
|
||||||
'used': 4.0,
|
},
|
||||||
'stake': 'BTC',
|
{
|
||||||
|
'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,
|
'free': 5.0,
|
||||||
'currency': 'USDT',
|
'balance': 10.0,
|
||||||
'est_stake': 0.0011563153318162476,
|
'currency': 'USDT',
|
||||||
'used': 5.0,
|
'est_stake': 0.0011563153318162476,
|
||||||
'stake': 'BTC',
|
'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
|
assert result['total'] == 12.309096315331816
|
||||||
|
|
||||||
|
@ -453,6 +453,10 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers):
|
|||||||
'used': 0.0,
|
'used': 0.0,
|
||||||
'est_stake': 12.0,
|
'est_stake': 12.0,
|
||||||
'stake': 'BTC',
|
'stake': 'BTC',
|
||||||
|
'is_position': False,
|
||||||
|
'leverage': 1.0,
|
||||||
|
'position': 0.0,
|
||||||
|
'side': 'long',
|
||||||
}
|
}
|
||||||
assert 'starting_capital' in response
|
assert 'starting_capital' in response
|
||||||
assert 'starting_capital_fiat' in response
|
assert 'starting_capital_fiat' in response
|
||||||
|
@ -905,6 +905,10 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None
|
|||||||
'balance': i,
|
'balance': i,
|
||||||
'est_stake': 1,
|
'est_stake': 1,
|
||||||
'stake': 'BTC',
|
'stake': 'BTC',
|
||||||
|
'is_position': False,
|
||||||
|
'leverage': 1.0,
|
||||||
|
'position': 0.0,
|
||||||
|
'side': 'long',
|
||||||
})
|
})
|
||||||
mocker.patch('freqtrade.rpc.rpc.RPC._rpc_balance', return_value={
|
mocker.patch('freqtrade.rpc.rpc.RPC._rpc_balance', return_value={
|
||||||
'currencies': balances,
|
'currencies': balances,
|
||||||
|
@ -95,6 +95,7 @@ def test_enter_exit_side(fee, is_short):
|
|||||||
)
|
)
|
||||||
assert trade.enter_side == enter_side
|
assert trade.enter_side == enter_side
|
||||||
assert trade.exit_side == exit_side
|
assert trade.exit_side == exit_side
|
||||||
|
assert trade.trade_direction == 'short' if is_short else 'long'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
@ -6,7 +6,7 @@ import pytest
|
|||||||
|
|
||||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
|
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
|
||||||
from freqtrade.exceptions import DependencyException
|
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):
|
def test_sync_wallet_at_boot(mocker, default_conf):
|
||||||
@ -234,3 +234,125 @@ def test_get_starting_balance(mocker, default_conf, available_capital, closed_pr
|
|||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
assert freqtrade.wallets.get_starting_balance() == expected
|
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
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
fetch_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
|
||||||
|
|
||||||
|
|
||||||
|
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'
|
||||||
|
Loading…
Reference in New Issue
Block a user