Merge branch 'develop' into 'validate_whitelist'

This commit is contained in:
Matthias
2019-03-21 06:22:48 +01:00
16 changed files with 207 additions and 103 deletions

View File

@@ -20,6 +20,7 @@ REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList']
DRY_RUN_WALLET = 999.9
TICKER_INTERVAL_MINUTES = {
'1m': 1,
@@ -60,6 +61,7 @@ CONF_SCHEMA = {
},
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
'dry_run': {'type': 'boolean'},
'dry_run_wallet': {'type': 'number'},
'process_only_new_candles': {'type': 'boolean'},
'minimal_roi': {
'type': 'object',

View File

@@ -66,7 +66,7 @@ def retrier(f):
class Exchange(object):
_conf: Dict = {}
_config: Dict = {}
_params: Dict = {}
# Dict to specify which options each exchange implements
@@ -82,7 +82,7 @@ class Exchange(object):
it does basic validation whether the specified exchange and pairs are valid.
:return: None
"""
self._conf.update(config)
self._config.update(config)
self._cached_ticker: Dict[str, Any] = {}
@@ -370,7 +370,7 @@ class Exchange(object):
def buy(self, pair: str, ordertype: str, amount: float,
rate: float, time_in_force) -> Dict:
if self._conf['dry_run']:
if self._config['dry_run']:
dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate)
return dry_order
@@ -383,7 +383,7 @@ class Exchange(object):
def sell(self, pair: str, ordertype: str, amount: float,
rate: float, time_in_force='gtc') -> Dict:
if self._conf['dry_run']:
if self._config['dry_run']:
dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate)
return dry_order
@@ -408,7 +408,7 @@ class Exchange(object):
raise OperationalException(
'In stoploss limit order, stop price should be more than limit price')
if self._conf['dry_run']:
if self._config['dry_run']:
dry_order = self.dry_run_order(
pair, ordertype, "sell", amount, stop_price)
return dry_order
@@ -423,8 +423,8 @@ class Exchange(object):
@retrier
def get_balance(self, currency: str) -> float:
if self._conf['dry_run']:
return 999.9
if self._config['dry_run']:
return constants.DRY_RUN_WALLET
# ccxt exception is already handled by get_balances
balances = self.get_balances()
@@ -436,7 +436,7 @@ class Exchange(object):
@retrier
def get_balances(self) -> dict:
if self._conf['dry_run']:
if self._config['dry_run']:
return {}
try:
@@ -607,7 +607,7 @@ class Exchange(object):
@retrier
def cancel_order(self, order_id: str, pair: str) -> None:
if self._conf['dry_run']:
if self._config['dry_run']:
return
try:
@@ -623,7 +623,7 @@ class Exchange(object):
@retrier
def get_order(self, order_id: str, pair: str) -> Dict:
if self._conf['dry_run']:
if self._config['dry_run']:
order = self._dry_run_open_orders[order_id]
return order
try:
@@ -660,7 +660,7 @@ class Exchange(object):
@retrier
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
if self._conf['dry_run']:
if self._config['dry_run']:
return []
if not self.exchange_has('fetchMyTrades'):
return []

View File

@@ -63,7 +63,7 @@ class FreqtradeBot(object):
exchange_name = self.config.get('exchange', {}).get('name', 'bittrex').title()
self.exchange = ExchangeResolver(exchange_name, self.config).exchange
self.wallets = Wallets(self.exchange)
self.wallets = Wallets(self.config, self.exchange)
self.dataprovider = DataProvider(self.config, self.exchange)
# Attach Dataprovider to Strategy baseclass
@@ -613,6 +613,25 @@ class FreqtradeBot(object):
f"(from {order_amount} to {real_amount}) from Trades")
return real_amount
def get_sell_rate(self, pair: str, refresh: bool) -> float:
"""
Get sell rate - either using get-ticker bid or first bid based on orderbook
The orderbook portion is only used for rpc messaging, which would otherwise fail
for BitMex (has no bid/ask in get_ticker)
or remain static in any other case since it's not updating.
:return: Bid rate
"""
config_ask_strategy = self.config.get('ask_strategy', {})
if config_ask_strategy.get('use_order_book', False):
logger.debug('Using order book to get sell rate')
order_book = self.exchange.get_order_book(pair, 1)
rate = order_book['bids'][0][0]
else:
rate = self.exchange.get_ticker(pair, refresh)['bid']
return rate
def handle_trade(self, trade: Trade) -> bool:
"""
Sells the current pair if the threshold is reached and updates the trade record.
@@ -649,7 +668,7 @@ class FreqtradeBot(object):
else:
logger.debug('checking sell')
sell_rate = self.exchange.get_ticker(trade.pair)['bid']
sell_rate = self.get_sell_rate(trade.pair, True)
if self.check_sell(trade, sell_rate, buy, sell):
return True
@@ -892,7 +911,8 @@ class FreqtradeBot(object):
"""
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit_trade = trade.calc_profit(rate=profit_rate)
current_rate = self.exchange.get_ticker(trade.pair)['bid']
# Use cached ticker here - it was updated seconds ago.
current_rate = self.get_sell_rate(trade.pair, False)
profit_percent = trade.calc_profit_percent(profit_rate)
gain = "profit" if profit_percent > 0 else "loss"

View File

@@ -94,7 +94,7 @@ class RPC(object):
order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair)
# calculate profit and send message to user
try:
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
except DependencyException:
current_rate = NAN
current_profit = trade.calc_profit_percent(current_rate)
@@ -125,7 +125,7 @@ class RPC(object):
for trade in trades:
# calculate profit and send message to user
try:
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
except DependencyException:
current_rate = NAN
trade_perc = (100 * trade.calc_profit_percent(current_rate))
@@ -213,7 +213,7 @@ class RPC(object):
else:
# Get current rate
try:
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
except DependencyException:
current_rate = NAN
profit_percent = trade.calc_profit_percent(rate=current_rate)
@@ -280,9 +280,9 @@ class RPC(object):
else:
try:
if coin == 'USDT':
rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid']
rate = 1.0 / self._freqtrade.get_sell_rate('BTC/USDT', False)
else:
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
rate = self._freqtrade.get_sell_rate(coin + '/BTC', False)
except (TemporaryError, DependencyException):
continue
est_btc: float = rate * balance['total']
@@ -328,6 +328,16 @@ class RPC(object):
self._freqtrade.state = State.RELOAD_CONF
return {'status': 'reloading config ...'}
def _rpc_stopbuy(self) -> Dict[str, str]:
"""
Handler to stop buying, but handle open trades gracefully.
"""
if self._freqtrade.state == State.RUNNING:
# Set 'max_open_trades' to 0
self._freqtrade.config['max_open_trades'] = 0
return {'status': 'No more buy will occur from now. Run /reload_conf to reset.'}
def _rpc_forcesell(self, trade_id) -> None:
"""
Handler for forcesell <id>.
@@ -356,7 +366,7 @@ class RPC(object):
return
# Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL)
# ---- EOF def _exec_forcesell ----

View File

@@ -91,6 +91,7 @@ class Telegram(RPC):
CommandHandler('daily', self._daily),
CommandHandler('count', self._count),
CommandHandler('reload_conf', self._reload_conf),
CommandHandler('stopbuy', self._stopbuy),
CommandHandler('whitelist', self._whitelist),
CommandHandler('help', self._help),
CommandHandler('version', self._version),
@@ -362,6 +363,18 @@ class Telegram(RPC):
msg = self._rpc_reload_conf()
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
@authorized_only
def _stopbuy(self, bot: Bot, update: Update) -> None:
"""
Handler for /stop_buy.
Sets max_open_trades to 0 and gracefully sells all open trades
:param bot: telegram bot
:param update: message update
:return: None
"""
msg = self._rpc_stopbuy()
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
@authorized_only
def _forcesell(self, bot: Bot, update: Update) -> None:
"""
@@ -481,6 +494,7 @@ class Telegram(RPC):
"*/count:* `Show number of trades running compared to allowed number of trades`" \
"\n" \
"*/balance:* `Show account balance per currency`\n" \
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \
"*/reload_conf:* `Reload configuration file` \n" \
"*/whitelist:* `Show current whitelist` \n" \
"*/help:* `This help message`\n" \

View File

@@ -406,6 +406,26 @@ def test_rpc_stop(mocker, default_conf) -> None:
assert freqtradebot.state == State.STOPPED
def test_rpc_stopbuy(mocker, default_conf) -> None:
patch_coinmarketcap(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_ticker=MagicMock()
)
freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
rpc = RPC(freqtradebot)
freqtradebot.state = State.RUNNING
assert freqtradebot.config['max_open_trades'] != 0
result = rpc._rpc_stopbuy()
assert {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} == result
assert freqtradebot.config['max_open_trades'] == 0
def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
patch_coinmarketcap(mocker)
patch_exchange(mocker)

View File

@@ -74,7 +74,7 @@ def test_init(default_conf, mocker, caplog) -> None:
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \
"['performance'], ['daily'], ['count'], ['reload_conf'], " \
"['whitelist'], ['help'], ['version']]"
"['stopbuy'], ['whitelist'], ['help'], ['version']]"
assert log_has(message_str, caplog.record_tuples)
@@ -662,6 +662,26 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
assert 'already stopped' in msg_mock.call_args_list[0][0][0]
def test_stopbuy_handle(default_conf, update, mocker) -> None:
patch_coinmarketcap(mocker)
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
assert freqtradebot.config['max_open_trades'] != 0
telegram._stopbuy(bot=MagicMock(), update=update)
assert freqtradebot.config['max_open_trades'] == 0
assert msg_mock.call_count == 1
assert 'No more buy will occur from now. Run /reload_conf to reset.' \
in msg_mock.call_args_list[0][0][0]
def test_reload_conf_handle(default_conf, update, mocker) -> None:
patch_coinmarketcap(mocker)
msg_mock = MagicMock()

View File

@@ -2962,6 +2962,31 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order
assert freqtrade.handle_trade(trade) is True
def test_get_sell_rate(default_conf, mocker, ticker, order_book_l2) -> None:
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_order_book=order_book_l2,
get_ticker=ticker,
)
pair = "ETH/BTC"
# Test regular mode
ft = get_patched_freqtradebot(mocker, default_conf)
rate = ft.get_sell_rate(pair, True)
assert isinstance(rate, float)
assert rate == 0.00001098
# Test orderbook mode
default_conf['ask_strategy']['use_order_book'] = True
default_conf['ask_strategy']['order_book_min'] = 1
default_conf['ask_strategy']['order_book_max'] = 2
ft = get_patched_freqtradebot(mocker, default_conf)
rate = ft.get_sell_rate(pair, True)
assert isinstance(rate, float)
assert rate == 0.043936
def test_startup_messages(default_conf, mocker):
default_conf['pairlist'] = {'method': 'VolumePairList',
'config': {'number_assets': 20}

View File

@@ -23,13 +23,13 @@ def test_sync_wallet_at_boot(mocker, default_conf):
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
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
assert freqtrade.wallets.get_free('BNT') == 1.0
mocker.patch.multiple(
@@ -50,13 +50,13 @@ def test_sync_wallet_at_boot(mocker, default_conf):
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
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
assert freqtrade.wallets.get_free('GAS') == 0.270739
assert freqtrade.wallets.get_used('GAS') == 0.1
assert freqtrade.wallets.get_total('GAS') == 0.260439
@@ -81,11 +81,11 @@ def test_sync_wallet_missing_data(mocker, default_conf):
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
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
assert freqtrade.wallets.get_free('GAS') == 0.260739

View File

@@ -1,15 +1,16 @@
# pragma pylint: disable=W0603
""" Wallet """
import logging
from typing import Dict, Any, NamedTuple
from typing import Dict, NamedTuple
from freqtrade.exchange import Exchange
from freqtrade import constants
logger = logging.getLogger(__name__)
# wallet data structure
class Wallet(NamedTuple):
exchange: str
currency: str
free: float = 0
used: float = 0
@@ -18,17 +19,19 @@ class Wallet(NamedTuple):
class Wallets(object):
def __init__(self, exchange: Exchange) -> None:
self.exchange = exchange
self.wallets: Dict[str, Any] = {}
def __init__(self, config: dict, exchange: Exchange) -> None:
self._config = config
self._exchange = exchange
self._wallets: Dict[str, Wallet] = {}
self.update()
def get_free(self, currency) -> float:
if self.exchange._conf['dry_run']:
return 999.9
if self._config['dry_run']:
return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET)
balance = self.wallets.get(currency)
balance = self._wallets.get(currency)
if balance and balance.free:
return balance.free
else:
@@ -36,10 +39,10 @@ class Wallets(object):
def get_used(self, currency) -> float:
if self.exchange._conf['dry_run']:
return 999.9
if self._config['dry_run']:
return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET)
balance = self.wallets.get(currency)
balance = self._wallets.get(currency)
if balance and balance.used:
return balance.used
else:
@@ -47,25 +50,25 @@ class Wallets(object):
def get_total(self, currency) -> float:
if self.exchange._conf['dry_run']:
return 999.9
if self._config['dry_run']:
return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET)
balance = self.wallets.get(currency)
balance = self._wallets.get(currency)
if balance and balance.total:
return balance.total
else:
return 0
def update(self) -> None:
balances = self.exchange.get_balances()
balances = self._exchange.get_balances()
for currency in balances:
self.wallets[currency] = Wallet(
self.exchange.id,
self._wallets[currency] = Wallet(
currency,
balances[currency].get('free', None),
balances[currency].get('used', None),
balances[currency].get('total', None)
)
logger.info('Wallets synced ...')
logger.info('Wallets synced.')