diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b6c18c2d9..8a2db84a9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -17,6 +17,7 @@ from cachetools import TTLCache, cached from freqtrade import (DependencyException, OperationalException, TemporaryError, __version__, constants, persistence) from freqtrade.exchange import Exchange +from freqtrade.wallets import Wallets from freqtrade.edge import Edge from freqtrade.persistence import Trade from freqtrade.rpc import RPCManager, RPCMessageType @@ -56,6 +57,7 @@ class FreqtradeBot(object): self.rpc: RPCManager = RPCManager(self) self.persistence = None self.exchange = Exchange(self.config) + self.wallets = Wallets(self.exchange) # Initializing Edge only if enabled self.edge = Edge(self.config, self.exchange, self.strategy) if \ @@ -335,7 +337,9 @@ class FreqtradeBot(object): else: stake_amount = self.config['stake_amount'] + # TODO: should come from the wallet avaliable_amount = self.exchange.get_balance(self.config['stake_currency']) + # avaliable_amount = self.wallets.wallets[self.config['stake_currency']].free if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: open_trades = len(Trade.query.filter(Trade.is_open.is_(True)).all()) @@ -503,6 +507,10 @@ class FreqtradeBot(object): ) Trade.session.add(trade) Trade.session.flush() + + # Updating wallets + self.wallets.update() + return True def process_maybe_execute_buy(self) -> bool: @@ -547,7 +555,14 @@ class FreqtradeBot(object): if trade.is_open and trade.open_order_id is None: # Check if we can sell our current pair - return self.handle_trade(trade) + result = self.handle_trade(trade) + + # Updating wallets if any trade occured + if result: + self.wallets.update() + + return result + except DependencyException as exception: logger.warning('Unable to sell trade: %s', exception) return False @@ -686,14 +701,17 @@ class FreqtradeBot(object): # Check if trade is still actually open if int(order['remaining']) == 0: + self.wallets.update() continue # Check if trade is still actually open if order['status'] == 'open': if order['side'] == 'buy' and ordertime < buy_timeoutthreashold: self.handle_timedout_limit_buy(trade, order) + self.wallets.update() elif order['side'] == 'sell' and ordertime < sell_timeoutthreashold: self.handle_timedout_limit_sell(trade, order) + self.wallets.update() # FIX: 20180110, why is cancel.order unconditionally here, whereas # it is conditionally called in the diff --git a/freqtrade/tests/test_wallets.py b/freqtrade/tests/test_wallets.py new file mode 100644 index 000000000..cc10d665c --- /dev/null +++ b/freqtrade/tests/test_wallets.py @@ -0,0 +1,84 @@ +# pragma pylint: disable=missing-docstring +from freqtrade.tests.conftest import get_patched_freqtradebot +from unittest.mock import MagicMock + + +def test_sync_wallet_at_boot(mocker, default_conf): + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value={ + "BNT": { + "free": 1.0, + "used": 2.0, + "total": 3.0 + }, + "GAS": { + "free": 0.260739, + "used": 0.0, + "total": 0.260739 + }, + }) + ) + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + assert len(freqtrade.wallets.wallets) == 2 + assert freqtrade.wallets.wallets['BNT'].free == 1.0 + assert freqtrade.wallets.wallets['BNT'].used == 2.0 + assert freqtrade.wallets.wallets['BNT'].total == 3.0 + assert freqtrade.wallets.wallets['GAS'].free == 0.260739 + assert freqtrade.wallets.wallets['GAS'].used == 0.0 + assert freqtrade.wallets.wallets['GAS'].total == 0.260739 + + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value={ + "BNT": { + "free": 1.2, + "used": 1.9, + "total": 3.5 + }, + "GAS": { + "free": 0.270739, + "used": 0.1, + "total": 0.260439 + }, + }) + ) + + freqtrade.wallets.update() + + assert len(freqtrade.wallets.wallets) == 2 + assert freqtrade.wallets.wallets['BNT'].free == 1.2 + assert freqtrade.wallets.wallets['BNT'].used == 1.9 + assert freqtrade.wallets.wallets['BNT'].total == 3.5 + assert freqtrade.wallets.wallets['GAS'].free == 0.270739 + assert freqtrade.wallets.wallets['GAS'].used == 0.1 + assert freqtrade.wallets.wallets['GAS'].total == 0.260439 + + +def test_sync_wallet_missing_data(mocker, default_conf): + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value={ + "BNT": { + "free": 1.0, + "used": 2.0, + "total": 3.0 + }, + "GAS": { + "free": 0.260739, + "total": 0.260739 + }, + }) + ) + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + assert len(freqtrade.wallets.wallets) == 2 + assert freqtrade.wallets.wallets['BNT'].free == 1.0 + assert freqtrade.wallets.wallets['BNT'].used == 2.0 + assert freqtrade.wallets.wallets['BNT'].total == 3.0 + assert freqtrade.wallets.wallets['GAS'].free == 0.260739 + assert freqtrade.wallets.wallets['GAS'].used is None + assert freqtrade.wallets.wallets['GAS'].total == 0.260739 diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py new file mode 100644 index 000000000..82f527d2c --- /dev/null +++ b/freqtrade/wallets.py @@ -0,0 +1,44 @@ +# pragma pylint: disable=W0603 +""" Wallet """ +import logging +from typing import Dict, Any, NamedTuple +from collections import namedtuple +from freqtrade.exchange import Exchange + +logger = logging.getLogger(__name__) + + +class Wallet(NamedTuple): + exchange: str + currency: str + free: float = 0 + used: float = 0 + total: float = 0 + + +class Wallets(object): + + # wallet data structure + wallet = namedtuple( + 'wallet', + ['exchange', 'currency', 'free', 'used', 'total'] + ) + + def __init__(self, exchange: Exchange) -> None: + self.exchange = exchange + self.wallets: Dict[str, Any] = {} + self.update() + + def update(self) -> None: + balances = self.exchange.get_balances() + + for currency in balances: + self.wallets[currency] = Wallet( + self.exchange.id, + currency, + balances[currency].get('free', None), + balances[currency].get('used', None), + balances[currency].get('total', None) + ) + + logger.info('Wallets synced ...')