telegram refactor 1/ (#389)

* telegram refactor 1/

move out freqcode from telegram

* telegram refactor 2/

move out rpc_trade_status

* telegram refactor 3/

move out rpc_daily_profit

* telegram refactor /4

move out rpc_trade_statistics

* 5/

* rpc refactor 6/

* rpc refactor 7/

* rpc refactor 8/

* rpc refactor 9/

* rpc refactor 10/

cleanups
two tests are broken

* fiat

* rpc: Add back fiat singleton usage

* test: rpc_trade_statistics

Test that rpc_trade_statistics can handle trades that lacks
trade.open_rate (it is set to None)

* test: rpc_forcesell

Also some cleanups

* test: telegram.py::init

* test: telegram test_cleanup and test_status

* test rcp cleanup
This commit is contained in:
kryofly 2018-02-01 07:05:23 +01:00 committed by Janne Sinivirta
parent 0a42a0e814
commit 9f6aedea47
5 changed files with 877 additions and 339 deletions

View File

@ -1,10 +1,21 @@
import logging import logging
import re
import arrow
from decimal import Decimal
from datetime import datetime, timedelta
from pandas import DataFrame
import sqlalchemy as sql
# from sqlalchemy import and_, func, text
from freqtrade.persistence import Trade
from freqtrade.misc import State, get_state, update_state
from freqtrade import exchange
from freqtrade.fiat_convert import CryptoToFiatConverter
from . import telegram from . import telegram
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_FIAT_CONVERT = CryptoToFiatConverter()
REGISTERED_MODULES = [] REGISTERED_MODULES = []
@ -40,3 +51,365 @@ def send_msg(msg: str) -> None:
logger.info(msg) logger.info(msg)
if 'telegram' in REGISTERED_MODULES: if 'telegram' in REGISTERED_MODULES:
telegram.send_msg(msg) telegram.send_msg(msg)
def shorten_date(_date):
"""
Trim the date so it fits on small screens
"""
new_date = re.sub('seconds?', 'sec', _date)
new_date = re.sub('minutes?', 'min', new_date)
new_date = re.sub('hours?', 'h', new_date)
new_date = re.sub('days?', 'd', new_date)
new_date = re.sub('^an?', '1', new_date)
return new_date
#
# Below follows the RPC backend
# it is prefixed with rpc_
# to raise awareness that it is
# a remotely exposed function
def rpc_trade_status():
# Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if get_state() != State.RUNNING:
return (True, '*Status:* `trader is not running`')
elif not trades:
return (True, '*Status:* `no active trade`')
else:
result = []
for trade in trades:
order = None
if trade.open_order_id:
order = exchange.get_order(trade.open_order_id)
# calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair, False)['bid']
current_profit = trade.calc_profit_percent(current_rate)
fmt_close_profit = '{:.2f}%'.format(
round(trade.close_profit * 100, 2)
) if trade.close_profit else None
message = """
*Trade ID:* `{trade_id}`
*Current Pair:* [{pair}]({market_url})
*Open Since:* `{date}`
*Amount:* `{amount}`
*Open Rate:* `{open_rate:.8f}`
*Close Rate:* `{close_rate}`
*Current Rate:* `{current_rate:.8f}`
*Close Profit:* `{close_profit}`
*Current Profit:* `{current_profit:.2f}%`
*Open Order:* `{open_order}`
""".format(
trade_id=trade.id,
pair=trade.pair,
market_url=exchange.get_pair_detail_url(trade.pair),
date=arrow.get(trade.open_date).humanize(),
open_rate=trade.open_rate,
close_rate=trade.close_rate,
current_rate=current_rate,
amount=round(trade.amount, 8),
close_profit=fmt_close_profit,
current_profit=round(current_profit * 100, 2),
open_order='({} rem={:.8f})'.format(
order['type'], order['remaining']
) if order else None,
)
result.append(message)
return (False, result)
def rpc_status_table():
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if get_state() != State.RUNNING:
return (True, '*Status:* `trader is not running`')
elif not trades:
return (True, '*Status:* `no active order`')
else:
trades_list = []
for trade in trades:
# calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair, False)['bid']
trades_list.append([
trade.id,
trade.pair,
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate))
])
columns = ['ID', 'Pair', 'Since', 'Profit']
df_statuses = DataFrame.from_records(trades_list, columns=columns)
df_statuses = df_statuses.set_index(columns[0])
# The style used throughout is to return a tuple
# consisting of (error_occured?, result)
# Another approach would be to just return the
# result, or raise error
return (False, df_statuses)
def rpc_daily_profit(timescale, stake_currency, fiat_display_currency):
today = datetime.utcnow().date()
profit_days = {}
if not (isinstance(timescale, int) and timescale > 0):
return (True, '*Daily [n]:* `must be an integer greater than 0`')
fiat = _FIAT_CONVERT
for day in range(0, timescale):
profitday = today - timedelta(days=day)
trades = Trade.query \
.filter(Trade.is_open.is_(False)) \
.filter(Trade.close_date >= profitday)\
.filter(Trade.close_date < (profitday + timedelta(days=1)))\
.order_by(Trade.close_date)\
.all()
curdayprofit = sum(trade.calc_profit() for trade in trades)
profit_days[profitday] = {
'amount': format(curdayprofit, '.8f'),
'trades': len(trades)
}
stats = [
[
key,
'{value:.8f} {symbol}'.format(
value=float(value['amount']),
symbol=stake_currency
),
'{value:.3f} {symbol}'.format(
value=fiat.convert_amount(
value['amount'],
stake_currency,
fiat_display_currency
),
symbol=fiat_display_currency
),
'{value} trade{s}'.format(value=value['trades'], s='' if value['trades'] < 2 else 's'),
]
for key, value in profit_days.items()
]
return (False, stats)
def rpc_trade_statistics(stake_currency, fiat_display_currency) -> None:
"""
:return: cumulative profit statistics.
"""
trades = Trade.query.order_by(Trade.id).all()
profit_all_coin = []
profit_all_percent = []
profit_closed_coin = []
profit_closed_percent = []
durations = []
for trade in trades:
current_rate = None
if not trade.open_rate:
continue
if trade.close_date:
durations.append((trade.close_date - trade.open_date).total_seconds())
if not trade.is_open:
profit_percent = trade.calc_profit_percent()
profit_closed_coin.append(trade.calc_profit())
profit_closed_percent.append(profit_percent)
else:
# Get current rate
current_rate = exchange.get_ticker(trade.pair, False)['bid']
profit_percent = trade.calc_profit_percent(rate=current_rate)
profit_all_coin.append(trade.calc_profit(rate=Decimal(trade.close_rate or current_rate)))
profit_all_percent.append(profit_percent)
best_pair = Trade.session.query(Trade.pair,
sql.func.sum(Trade.close_profit).label('profit_sum')) \
.filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(sql.text('profit_sum DESC')) \
.first()
if not best_pair:
return (True, '*Status:* `no closed trade`')
bp_pair, bp_rate = best_pair
# FIX: we want to keep fiatconverter in a state/environment,
# doing this will utilize its caching functionallity, instead we reinitialize it here
fiat = _FIAT_CONVERT
# Prepare data to display
profit_closed_coin = round(sum(profit_closed_coin), 8)
profit_closed_percent = round(sum(profit_closed_percent) * 100, 2)
profit_closed_fiat = fiat.convert_amount(
profit_closed_coin,
stake_currency,
fiat_display_currency
)
profit_all_coin = round(sum(profit_all_coin), 8)
profit_all_percent = round(sum(profit_all_percent) * 100, 2)
profit_all_fiat = fiat.convert_amount(
profit_all_coin,
stake_currency,
fiat_display_currency
)
num = float(len(durations) or 1)
return (False,
{'profit_closed_coin': profit_closed_coin,
'profit_closed_percent': profit_closed_percent,
'profit_closed_fiat': profit_closed_fiat,
'profit_all_coin': profit_all_coin,
'profit_all_percent': profit_all_percent,
'profit_all_fiat': profit_all_fiat,
'trade_count': len(trades),
'first_trade_date': arrow.get(trades[0].open_date).humanize(),
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
'avg_duration': str(timedelta(seconds=sum(durations) /
num)).split('.')[0],
'best_pair': bp_pair,
'best_rate': round(bp_rate * 100, 2)
})
def rpc_balance(fiat_display_currency):
"""
:return: current account balance per crypto
"""
balances = [
c for c in exchange.get_balances()
if c['Balance'] or c['Available'] or c['Pending']
]
if not balances:
return (True, '`All balances are zero.`')
output = []
total = 0.0
for currency in balances:
coin = currency['Currency']
if coin == 'BTC':
currency["Rate"] = 1.0
else:
if coin == 'USDT':
currency["Rate"] = 1.0 / exchange.get_ticker('USDT_BTC', False)['bid']
else:
currency["Rate"] = exchange.get_ticker('BTC_' + coin, False)['bid']
currency['BTC'] = currency["Rate"] * currency["Balance"]
total = total + currency['BTC']
output.append({'currency': currency['Currency'],
'available': currency['Available'],
'balance': currency['Balance'],
'pending': currency['Pending'],
'est_btc': currency['BTC']
})
fiat = _FIAT_CONVERT
symbol = fiat_display_currency
value = fiat.convert_amount(total, 'BTC', symbol)
return (False, (output, total, symbol, value))
def rpc_start():
"""
Handler for start.
"""
if get_state() == State.RUNNING:
return (True, '*Status:* `already running`')
else:
update_state(State.RUNNING)
def rpc_stop():
"""
Handler for stop.
"""
if get_state() == State.RUNNING:
update_state(State.STOPPED)
return (False, '`Stopping trader ...`')
else:
return (True, '*Status:* `already stopped`')
# FIX: no test for this!!!!
def rpc_forcesell(trade_id) -> None:
"""
Handler for forcesell <id>.
Sells the given trade at current price
:return: error or None
"""
def _exec_forcesell(trade: Trade) -> str:
# Check if there is there is an open order
if trade.open_order_id:
order = exchange.get_order(trade.open_order_id)
# Cancel open LIMIT_BUY orders and close trade
if order and not order['closed'] and order['type'] == 'LIMIT_BUY':
exchange.cancel_order(trade.open_order_id)
trade.close(order.get('rate') or trade.open_rate)
# TODO: sell amount which has been bought already
return
# Ignore trades with an attached LIMIT_SELL order
if order and not order['closed'] and order['type'] == 'LIMIT_SELL':
return
# Get current rate and execute sell
current_rate = exchange.get_ticker(trade.pair, False)['bid']
from freqtrade.main import execute_sell
execute_sell(trade, current_rate)
# ---- EOF def _exec_forcesell ----
if get_state() != State.RUNNING:
return (True, '`trader is not running`')
if trade_id == 'all':
# Execute sell for all open orders
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
_exec_forcesell(trade)
return (False, '')
# Query for trade
trade = Trade.query.filter(sql.and_(
Trade.id == trade_id,
Trade.is_open.is_(True)
)).first()
if not trade:
logger.warning('forcesell: Invalid argument received')
return (True, 'Invalid argument.')
_exec_forcesell(trade)
return (False, '')
def rpc_performance() -> None:
"""
Handler for performance.
Shows a performance statistic from finished trades
"""
if get_state() != State.RUNNING:
return (True, '`trader is not running`')
pair_rates = Trade.session.query(Trade.pair,
sql.func.sum(Trade.close_profit).label('profit_sum'),
sql.func.count(Trade.pair).label('count')) \
.filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(sql.text('profit_sum DESC')) \
.all()
trades = []
for (pair, rate, count) in pair_rates:
trades.append({'pair': pair, 'profit': round(rate * 100, 2), 'count': count})
return (False, trades)
def rpc_count() -> None:
"""
Returns the number of trades running
:return: None
"""
if get_state() != State.RUNNING:
return (True, '`trader is not running`')
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
return (False, trades)

View File

@ -1,21 +1,25 @@
import logging import logging
import re
from datetime import datetime, timedelta
from decimal import Decimal
from typing import Any, Callable from typing import Any, Callable
import arrow
from pandas import DataFrame
from sqlalchemy import and_, func, text
from tabulate import tabulate from tabulate import tabulate
from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update
from telegram.error import NetworkError, TelegramError from telegram.error import NetworkError, TelegramError
from telegram.ext import CommandHandler, Updater from telegram.ext import CommandHandler, Updater
from freqtrade import __version__, exchange from freqtrade.rpc.__init__ import (rpc_status_table,
from freqtrade.fiat_convert import CryptoToFiatConverter rpc_trade_status,
from freqtrade.misc import State, get_state, update_state rpc_daily_profit,
from freqtrade.persistence import Trade rpc_trade_statistics,
rpc_balance,
rpc_start,
rpc_stop,
rpc_forcesell,
rpc_performance,
rpc_count,
)
from freqtrade import __version__
# Remove noisy log messages # Remove noisy log messages
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO) logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
@ -24,7 +28,6 @@ logger = logging.getLogger(__name__)
_UPDATER: Updater = None _UPDATER: Updater = None
_CONF = {} _CONF = {}
_FIAT_CONVERT = CryptoToFiatConverter()
def init(config: dict) -> None: def init(config: dict) -> None:
@ -129,51 +132,12 @@ def _status(bot: Bot, update: Update) -> None:
return return
# Fetch open trade # Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all() (error, trades) = rpc_trade_status()
if get_state() != State.RUNNING: if error:
send_msg('*Status:* `trader is not running`', bot=bot) send_msg(trades, bot=bot)
elif not trades:
send_msg('*Status:* `no active trade`', bot=bot)
else: else:
for trade in trades: for trademsg in trades:
order = None send_msg(trademsg, bot=bot)
if trade.open_order_id:
order = exchange.get_order(trade.open_order_id)
# calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair, False)['bid']
current_profit = trade.calc_profit_percent(current_rate)
fmt_close_profit = '{:.2f}%'.format(
round(trade.close_profit * 100, 2)
) if trade.close_profit else None
message = """
*Trade ID:* `{trade_id}`
*Current Pair:* [{pair}]({pair_url})
*Open Since:* `{date}`
*Amount:* `{amount}`
*Open Rate:* `{open_rate:.8f}`
*Close Rate:* `{close_rate}`
*Current Rate:* `{current_rate:.8f}`
*Close Profit:* `{close_profit}`
*Current Profit:* `{current_profit:.2f}%`
*Open Order:* `{open_order}`
*Total Open Trades:* `{total_trades}`
""".format(
trade_id=trade.id,
pair=trade.pair,
pair_url=exchange.get_pair_detail_url(trade.pair),
date=arrow.get(trade.open_date).humanize(),
open_rate=trade.open_rate,
close_rate=trade.close_rate,
current_rate=current_rate,
amount=round(trade.amount, 8),
close_profit=fmt_close_profit,
current_profit=round(current_profit * 100, 2),
open_order='({} rem={:.8f})'.format(
order['type'], order['remaining']
) if order else None,
total_trades=len(trades)
)
send_msg(message, bot=bot)
@authorized_only @authorized_only
@ -186,27 +150,10 @@ def _status_table(bot: Bot, update: Update) -> None:
:return: None :return: None
""" """
# Fetch open trade # Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all() (err, df_statuses) = rpc_status_table()
if get_state() != State.RUNNING: if err:
send_msg('*Status:* `trader is not running`', bot=bot) send_msg(df_statuses, bot=bot)
elif not trades:
send_msg('*Status:* `no active order`', bot=bot)
else: else:
trades_list = []
for trade in trades:
# calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair, False)['bid']
trades_list.append([
trade.id,
trade.pair,
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate))
])
columns = ['ID', 'Pair', 'Since', 'Profit']
df_statuses = DataFrame.from_records(trades_list, columns=columns)
df_statuses = df_statuses.set_index(columns[0])
message = tabulate(df_statuses, headers='keys', tablefmt='simple') message = tabulate(df_statuses, headers='keys', tablefmt='simple')
message = "<pre>{}</pre>".format(message) message = "<pre>{}</pre>".format(message)
@ -222,61 +169,25 @@ def _daily(bot: Bot, update: Update) -> None:
:param update: message update :param update: message update
:return: None :return: None
""" """
today = datetime.utcnow().date()
profit_days = {}
try: try:
timescale = int(update.message.text.replace('/daily', '').strip()) timescale = int(update.message.text.replace('/daily', '').strip())
except (TypeError, ValueError): except (TypeError, ValueError):
timescale = 7 timescale = 7
(error, stats) = rpc_daily_profit(timescale,
if not (isinstance(timescale, int) and timescale > 0):
send_msg('*Daily [n]:* `must be an integer greater than 0`', bot=bot)
return
for day in range(0, timescale):
profitday = today - timedelta(days=day)
trades = Trade.query \
.filter(Trade.is_open.is_(False)) \
.filter(Trade.close_date >= profitday)\
.filter(Trade.close_date < (profitday + timedelta(days=1)))\
.order_by(Trade.close_date)\
.all()
curdayprofit = sum(trade.calc_profit() for trade in trades)
profit_days[profitday] = {
'amount': format(curdayprofit, '.8f'),
'trades': len(trades)
}
stats = [
[
key,
'{value:.8f} {symbol}'.format(
value=float(value['amount']),
symbol=_CONF['stake_currency']
),
'{value:.3f} {symbol}'.format(
value=_FIAT_CONVERT.convert_amount(
value['amount'],
_CONF['stake_currency'], _CONF['stake_currency'],
_CONF['fiat_display_currency'] _CONF['fiat_display_currency'])
), if error:
symbol=_CONF['fiat_display_currency'] send_msg(stats, bot=bot)
), else:
'{value} trade{s}'.format(value=value['trades'], s='' if value['trades'] < 2 else 's'),
]
for key, value in profit_days.items()
]
stats = tabulate(stats, stats = tabulate(stats,
headers=[ headers=[
'Day', 'Day',
'Profit {}'.format(_CONF['stake_currency']), 'Profit {}'.format(_CONF['stake_currency']),
'Profit {}'.format(_CONF['fiat_display_currency']), 'Profit {}'.format(_CONF['fiat_display_currency'])
'# Trades'
], ],
tablefmt='simple') tablefmt='simple')
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'.format(
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'.format(timescale, stats) timescale, stats)
send_msg(message, bot=bot, parse_mode=ParseMode.HTML) send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
@ -289,62 +200,12 @@ def _profit(bot: Bot, update: Update) -> None:
:param update: message update :param update: message update
:return: None :return: None
""" """
trades = Trade.query.order_by(Trade.id).all() (error, stats) = rpc_trade_statistics(_CONF['stake_currency'],
_CONF['fiat_display_currency'])
profit_all_coin = [] if error:
profit_all_percent = [] send_msg(stats, bot=bot)
profit_closed_coin = []
profit_closed_percent = []
durations = []
for trade in trades:
current_rate = None
if not trade.open_rate:
continue
if trade.close_date:
durations.append((trade.close_date - trade.open_date).total_seconds())
if not trade.is_open:
profit_percent = trade.calc_profit_percent()
profit_closed_coin.append(trade.calc_profit())
profit_closed_percent.append(profit_percent)
else:
# Get current rate
current_rate = exchange.get_ticker(trade.pair, False)['bid']
profit_percent = trade.calc_profit_percent(rate=current_rate)
profit_all_coin.append(trade.calc_profit(rate=Decimal(trade.close_rate or current_rate)))
profit_all_percent.append(profit_percent)
best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
.filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(text('profit_sum DESC')) \
.first()
if not best_pair:
send_msg('*Status:* `no closed trade`', bot=bot)
return return
bp_pair, bp_rate = best_pair
# Prepare data to display
profit_closed_coin = round(sum(profit_closed_coin), 8)
profit_closed_percent = round(sum(profit_closed_percent) * 100, 2)
profit_closed_fiat = _FIAT_CONVERT.convert_amount(
profit_closed_coin,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
)
profit_all_coin = round(sum(profit_all_coin), 8)
profit_all_percent = round(sum(profit_all_percent) * 100, 2)
profit_all_fiat = _FIAT_CONVERT.convert_amount(
profit_all_coin,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
)
# Message to display # Message to display
markdown_msg = """ markdown_msg = """
*ROI:* Close trades *ROI:* Close trades
@ -362,18 +223,18 @@ def _profit(bot: Bot, update: Update) -> None:
""".format( """.format(
coin=_CONF['stake_currency'], coin=_CONF['stake_currency'],
fiat=_CONF['fiat_display_currency'], fiat=_CONF['fiat_display_currency'],
profit_closed_coin=profit_closed_coin, profit_closed_coin=stats['profit_closed_coin'],
profit_closed_percent=profit_closed_percent, profit_closed_percent=stats['profit_closed_percent'],
profit_closed_fiat=profit_closed_fiat, profit_closed_fiat=stats['profit_closed_fiat'],
profit_all_coin=profit_all_coin, profit_all_coin=stats['profit_all_coin'],
profit_all_percent=profit_all_percent, profit_all_percent=stats['profit_all_percent'],
profit_all_fiat=profit_all_fiat, profit_all_fiat=stats['profit_all_fiat'],
trade_count=len(trades), trade_count=stats['trade_count'],
first_trade_date=arrow.get(trades[0].open_date).humanize(), first_trade_date=stats['first_trade_date'],
latest_trade_date=arrow.get(trades[-1].open_date).humanize(), latest_trade_date=stats['latest_trade_date'],
avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0], avg_duration=stats['avg_duration'],
best_pair=bp_pair, best_pair=stats['best_pair'],
best_rate=round(bp_rate * 100, 2), best_rate=stats['best_rate']
) )
send_msg(markdown_msg, bot=bot) send_msg(markdown_msg, bot=bot)
@ -382,41 +243,22 @@ def _profit(bot: Bot, update: Update) -> None:
def _balance(bot: Bot, update: Update) -> None: def _balance(bot: Bot, update: Update) -> None:
""" """
Handler for /balance Handler for /balance
Returns current account balance per crypto
""" """
output = '' (error, result) = rpc_balance(_CONF['fiat_display_currency'])
balances = [ if error:
c for c in exchange.get_balances()
if c['Balance'] or c['Available'] or c['Pending']
]
if not balances:
send_msg('`All balances are zero.`') send_msg('`All balances are zero.`')
return return
total = 0.0 (currencys, total, symbol, value) = result
for currency in balances: output = ''
coin = currency['Currency'] for currency in currencys:
if coin == 'BTC': output += """*Currency*: {currency}
currency["Rate"] = 1.0 *Available*: {available}
else: *Balance*: {balance}
if coin == 'USDT': *Pending*: {pending}
currency["Rate"] = 1.0 / exchange.get_ticker('USDT_BTC', False)['bid'] *Est. BTC*: {est_btc: .8f}
else:
currency["Rate"] = exchange.get_ticker('BTC_' + coin, False)['bid']
currency['BTC'] = currency["Rate"] * currency["Balance"]
total = total + currency['BTC']
output += """*Currency*: {Currency}
*Available*: {Available}
*Balance*: {Balance}
*Pending*: {Pending}
*Est. BTC*: {BTC: .8f}
""".format(**currency) """.format(**currency)
symbol = _CONF['fiat_display_currency']
value = _FIAT_CONVERT.convert_amount(
total, 'BTC', symbol
)
output += """*Estimated Value*: output += """*Estimated Value*:
*BTC*: {0: .8f} *BTC*: {0: .8f}
*{1}*: {2: .2f} *{1}*: {2: .2f}
@ -433,10 +275,9 @@ def _start(bot: Bot, update: Update) -> None:
:param update: message update :param update: message update
:return: None :return: None
""" """
if get_state() == State.RUNNING: (error, msg) = rpc_start()
send_msg('*Status:* `already running`', bot=bot) if error:
else: send_msg(msg, bot=bot)
update_state(State.RUNNING)
@authorized_only @authorized_only
@ -448,13 +289,11 @@ def _stop(bot: Bot, update: Update) -> None:
:param update: message update :param update: message update
:return: None :return: None
""" """
if get_state() == State.RUNNING: (error, msg) = rpc_stop()
send_msg('`Stopping trader ...`', bot=bot) send_msg(msg, bot=bot)
update_state(State.STOPPED)
else:
send_msg('*Status:* `already stopped`', bot=bot)
# FIX: no test for this!!!!
@authorized_only @authorized_only
def _forcesell(bot: Bot, update: Update) -> None: def _forcesell(bot: Bot, update: Update) -> None:
""" """
@ -464,29 +303,13 @@ def _forcesell(bot: Bot, update: Update) -> None:
:param update: message update :param update: message update
:return: None :return: None
""" """
if get_state() != State.RUNNING:
send_msg('`trader is not running`', bot=bot)
return
trade_id = update.message.text.replace('/forcesell', '').strip() trade_id = update.message.text.replace('/forcesell', '').strip()
if trade_id == 'all': (error, message) = rpc_forcesell(trade_id)
# Execute sell for all open orders if error:
for trade in Trade.query.filter(Trade.is_open.is_(True)).all(): send_msg(message, bot=bot)
_exec_forcesell(trade)
return return
# Query for trade
trade = Trade.query.filter(and_(
Trade.id == trade_id,
Trade.is_open.is_(True)
)).first()
if not trade:
send_msg('Invalid argument. See `/help` to view usage')
logger.warning('/forcesell: Invalid argument received')
return
_exec_forcesell(trade)
@authorized_only @authorized_only
def _performance(bot: Bot, update: Update) -> None: def _performance(bot: Bot, update: Update) -> None:
@ -497,26 +320,18 @@ def _performance(bot: Bot, update: Update) -> None:
:param update: message update :param update: message update
:return: None :return: None
""" """
if get_state() != State.RUNNING: (error, trades) = rpc_performance()
send_msg('`trader is not running`', bot=bot) if error:
send_msg(trades, bot=bot)
return return
pair_rates = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum'),
func.count(Trade.pair).label('count')) \
.filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(text('profit_sum DESC')) \
.all()
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format( stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
index=i + 1, index=i + 1,
pair=pair, pair=trade['pair'],
profit=round(rate * 100, 2), profit=trade['profit'],
count=count count=trade['count']
) for i, (pair, rate, count) in enumerate(pair_rates)) ) for i, trade in enumerate(trades))
message = '<b>Performance:</b>\n{}'.format(stats) message = '<b>Performance:</b>\n{}'.format(stats)
logger.debug(message)
send_msg(message, parse_mode=ParseMode.HTML) send_msg(message, parse_mode=ParseMode.HTML)
@ -529,12 +344,11 @@ def _count(bot: Bot, update: Update) -> None:
:param update: message update :param update: message update
:return: None :return: None
""" """
if get_state() != State.RUNNING: (error, trades) = rpc_count()
send_msg('`trader is not running`', bot=bot) if error:
send_msg(trades, bot=bot)
return return
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
message = tabulate({ message = tabulate({
'current': [len(trades)], 'current': [len(trades)],
'max': [_CONF['max_open_trades']] 'max': [_CONF['max_open_trades']]
@ -582,40 +396,6 @@ def _version(bot: Bot, update: Update) -> None:
send_msg('*Version:* `{}`'.format(__version__), bot=bot) send_msg('*Version:* `{}`'.format(__version__), bot=bot)
def shorten_date(_date):
"""
Trim the date so it fits on small screens
"""
new_date = re.sub('seconds?', 'sec', _date)
new_date = re.sub('minutes?', 'min', new_date)
new_date = re.sub('hours?', 'h', new_date)
new_date = re.sub('days?', 'd', new_date)
new_date = re.sub('^an?', '1', new_date)
return new_date
def _exec_forcesell(trade: Trade) -> None:
# Check if there is there is an open order
if trade.open_order_id:
order = exchange.get_order(trade.open_order_id)
# Cancel open LIMIT_BUY orders and close trade
if order and not order['closed'] and order['type'] == 'LIMIT_BUY':
exchange.cancel_order(trade.open_order_id)
trade.close(order.get('rate') or trade.open_rate)
# TODO: sell amount which has been bought already
return
# Ignore trades with an attached LIMIT_SELL order
if order and not order['closed'] and order['type'] == 'LIMIT_SELL':
return
# Get current rate and execute sell
current_rate = exchange.get_ticker(trade.pair, False)['bid']
from freqtrade.main import execute_sell
execute_sell(trade, current_rate)
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
""" """
Send given markdown message Send given markdown message

View File

@ -272,3 +272,10 @@ def default_strategy():
strategy = Strategy() strategy = Strategy()
strategy.init({'strategy': 'default_strategy'}) strategy.init({'strategy': 'default_strategy'})
return strategy return strategy
# FIX:
# Create an fixture/function
# that inserts a trade of some type and open-status
# return the open-order-id
# See tests in rpc/main that could use this

View File

@ -1,8 +1,21 @@
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103 # pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
from datetime import datetime
from copy import deepcopy from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
from sqlalchemy import create_engine
from freqtrade.rpc import init, cleanup, send_msg from freqtrade.rpc import init, cleanup, send_msg
from freqtrade.persistence import Trade
import freqtrade.main as main
import freqtrade.misc as misc
import freqtrade.rpc as rpc
def prec_satoshi(a, b):
"""
:return: True if A and B differs less than one satoshi.
"""
return abs(a - b) < 0.00000001
def test_init_telegram_enabled(default_conf, mocker): def test_init_telegram_enabled(default_conf, mocker):
@ -55,3 +68,343 @@ def test_send_msg_telegram_disabled(mocker):
telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock()) telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock())
send_msg('test') send_msg('test')
assert telegram_mock.call_count == 0 assert telegram_mock.call_count == 0
def test_rpc_forcesell(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock())
cancel_order_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
cancel_order=cancel_order_mock,
get_order=MagicMock(return_value={
'closed': True,
'type': 'LIMIT_BUY',
}))
main.init(default_conf, create_engine('sqlite://'))
misc.update_state(misc.State.STOPPED)
(error, res) = rpc.rpc_forcesell(None)
assert error
assert res == '`trader is not running`'
misc.update_state(misc.State.RUNNING)
(error, res) = rpc.rpc_forcesell(None)
assert error
assert res == 'Invalid argument.'
(error, res) = rpc.rpc_forcesell('all')
assert not error
assert res == ''
main.create_trade(0.001, 5)
(error, res) = rpc.rpc_forcesell('all')
assert not error
assert res == ''
(error, res) = rpc.rpc_forcesell('1')
assert not error
assert res == ''
misc.update_state(misc.State.STOPPED)
(error, res) = rpc.rpc_forcesell(None)
assert error
assert res == '`trader is not running`'
(error, res) = rpc.rpc_forcesell('all')
assert error
assert res == '`trader is not running`'
misc.update_state(misc.State.RUNNING)
assert cancel_order_mock.call_count == 0
# make an limit-buy open trade
mocker.patch.multiple('freqtrade.exchange',
get_order=MagicMock(return_value={
'closed': None,
'type': 'LIMIT_BUY'
}))
# check that the trade is called, which is done
# by ensuring exchange.cancel_order is called
(error, res) = rpc.rpc_forcesell('1')
assert not error
assert res == ''
assert cancel_order_mock.call_count == 1
main.create_trade(0.001, 5)
# make an limit-sell open trade
mocker.patch.multiple('freqtrade.exchange',
get_order=MagicMock(return_value={
'closed': None,
'type': 'LIMIT_SELL'
}))
(error, res) = rpc.rpc_forcesell('2')
assert not error
assert res == ''
# status quo, no exchange calls
assert cancel_order_mock.call_count == 1
def test_rpc_trade_status(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
main.init(default_conf, create_engine('sqlite://'))
misc.update_state(misc.State.STOPPED)
(error, result) = rpc.rpc_trade_status()
assert error
assert result.find('trader is not running') >= 0
misc.update_state(misc.State.RUNNING)
(error, result) = rpc.rpc_trade_status()
assert error
assert result.find('no active trade') >= 0
main.create_trade(0.001, 5)
(error, result) = rpc.rpc_trade_status()
assert not error
trade = result[0]
assert trade.find('[BTC_ETH]') >= 0
def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
main.init(default_conf, create_engine('sqlite://'))
stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency']
# Create some test data
main.create_trade(0.001, 5)
trade = Trade.query.first()
assert trade
# Simulate buy & sell
trade.update(limit_buy_order)
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
# Try valid data
update.message.text = '/daily 2'
(error, days) = rpc.rpc_daily_profit(7, stake_currency,
fiat_display_currency)
assert not error
assert len(days) == 7
for day in days:
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
assert (day[1] == '0.00000000 BTC' or
day[1] == '0.00006217 BTC')
assert (day[2] == '0.000 USD' or
day[2] == '0.933 USD')
# ensure first day is current date
assert str(days[0][0]) == str(datetime.utcnow().date())
# Try invalid data
(error, days) = rpc.rpc_daily_profit(0, stake_currency,
fiat_display_currency)
assert error
assert days.find('must be an integer greater than 0') >= 0
def test_rpc_trade_statistics(
default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
main.init(default_conf, create_engine('sqlite://'))
stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency']
(error, stats) = rpc.rpc_trade_statistics(stake_currency,
fiat_display_currency)
assert error
assert stats.find('no closed trade') >= 0
# Create some test data
main.create_trade(0.001, 5)
trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
# Update the ticker with a market going up
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up)
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
(error, stats) = rpc.rpc_trade_statistics(stake_currency,
fiat_display_currency)
assert not error
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
assert prec_satoshi(stats['profit_closed_percent'], 6.2)
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
assert prec_satoshi(stats['profit_all_coin'], 6.217e-05)
assert prec_satoshi(stats['profit_all_percent'], 6.2)
assert prec_satoshi(stats['profit_all_fiat'], 0.93255)
assert stats['trade_count'] == 1
assert stats['first_trade_date'] == 'just now'
assert stats['latest_trade_date'] == 'just now'
assert stats['avg_duration'] == '0:00:00'
assert stats['best_pair'] == 'BTC_ETH'
assert prec_satoshi(stats['best_rate'], 6.2)
# Test that rpc_trade_statistics can handle trades that lacks
# trade.open_rate (it is set to None)
def test_rpc_trade_statistics_closed(
default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
main.init(default_conf, create_engine('sqlite://'))
stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency']
# Create some test data
main.create_trade(0.001, 5)
trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
# Update the ticker with a market going up
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up)
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
for trade in Trade.query.order_by(Trade.id).all():
trade.open_rate = None
(error, stats) = rpc.rpc_trade_statistics(stake_currency,
fiat_display_currency)
assert not error
assert prec_satoshi(stats['profit_closed_coin'], 0)
assert prec_satoshi(stats['profit_closed_percent'], 0)
assert prec_satoshi(stats['profit_closed_fiat'], 0)
assert prec_satoshi(stats['profit_all_coin'], 0)
assert prec_satoshi(stats['profit_all_percent'], 0)
assert prec_satoshi(stats['profit_all_fiat'], 0)
assert stats['trade_count'] == 1
assert stats['first_trade_date'] == 'just now'
assert stats['latest_trade_date'] == 'just now'
assert stats['avg_duration'] == '0:00:00'
assert stats['best_pair'] == 'BTC_ETH'
assert prec_satoshi(stats['best_rate'], 6.2)
def test_rpc_balance_handle(default_conf, update, mocker):
mock_balance = [{
'Currency': 'BTC',
'Balance': 10.0,
'Available': 12.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
}, {
'Currency': 'ETH',
'Balance': 0.0,
'Available': 0.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
}]
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.exchange',
get_balances=MagicMock(return_value=mock_balance))
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
(error, res) = rpc.rpc_balance(default_conf['fiat_display_currency'])
assert not error
(trade, x, y, z) = res
assert prec_satoshi(x, 10)
assert prec_satoshi(z, 150000)
assert y == 'USD'
assert len(trade) == 1
assert trade[0]['currency'] == 'BTC'
assert prec_satoshi(trade[0]['available'], 12)
assert prec_satoshi(trade[0]['balance'], 10)
assert prec_satoshi(trade[0]['pending'], 0)
assert prec_satoshi(trade[0]['est_btc'], 10)
def test_performance_handle(
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
msg_mock = MagicMock()
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
main.init(default_conf, create_engine('sqlite://'))
# Create some test data
main.create_trade(0.001, int(default_conf['ticker_interval']))
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
(error, res) = rpc.rpc_performance()
assert not error
assert len(res) == 1
assert res[0]['pair'] == 'BTC_ETH'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit'], 6.2)

View File

@ -15,8 +15,9 @@ from freqtrade.misc import update_state, State, get_state
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc import telegram from freqtrade.rpc import telegram
from freqtrade.rpc.telegram import authorized_only, is_enabled, send_msg, _status, _status_table, \ from freqtrade.rpc.telegram import authorized_only, is_enabled, send_msg, _status, _status_table, \
_profit, _forcesell, _performance, _daily, _count, _start, _stop, _balance, _version, _help, \ _profit, _forcesell, _performance, _daily, _count, _start, _stop, _balance, _version, _help
_exec_forcesell
import freqtrade.rpc.telegram as tg
def test_is_enabled(default_conf, mocker): def test_is_enabled(default_conf, mocker):
@ -283,30 +284,6 @@ def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, m
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0] assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
def test_exec_forcesell_open_orders(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
cancel_order_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.exchange',
get_ticker=ticker,
get_order=MagicMock(return_value={
'closed': None,
'type': 'LIMIT_BUY',
}),
cancel_order=cancel_order_mock)
trade = Trade(
pair='BTC_ETH',
open_rate=1,
exchange='BITTREX',
open_order_id='123456789',
amount=1,
fee=0.0,
)
_exec_forcesell(trade)
assert cancel_order_mock.call_count == 1
assert trade.is_open is False
def test_forcesell_all_handle(default_conf, update, ticker, mocker): def test_forcesell_all_handle(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False))
@ -630,8 +607,7 @@ def test_stop_handle_already_stopped(default_conf, update, mocker):
assert 'already stopped' in msg_mock.call_args_list[0][0][0] assert 'already stopped' in msg_mock.call_args_list[0][0][0]
def test_balance_handle(default_conf, update, mocker): def test_telegram_balance_handle(default_conf, update, mocker):
mock_balance = [{ mock_balance = [{
'Currency': 'BTC', 'Currency': 'BTC',
'Balance': 10.0, 'Balance': 10.0,
@ -766,16 +742,65 @@ def test_send_msg_network_error(default_conf, mocker):
assert len(bot.method_calls) == 2 assert len(bot.method_calls) == 2
def test_init(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker): def test_init(default_conf, mocker):
start_polling = MagicMock()
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
# mock telegram.ext.Updater
Updater=MagicMock(return_value=start_polling))
# not enabled
tg.init(default_conf)
assert start_polling.call_count == 0
# number of handles registered
assert start_polling.dispatcher.add_handler.call_count == 11
assert start_polling.start_polling.call_count == 1
# enabled
default_conf['telegram'] = {}
default_conf['telegram']['enabled'] = True
default_conf['telegram']['token'] = ''
tg.init(default_conf)
def test_cleanup(default_conf, mocker):
default_conf['telegram'] = {}
default_conf['telegram']['enabled'] = False
updater_mock = MagicMock()
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
_UPDATER=updater_mock)
# not enabled
tg.cleanup()
assert updater_mock.stop.call_count == 0
# enabled
default_conf['telegram']['enabled'] = True
tg.cleanup()
assert updater_mock.stop.call_count == 1
def test_status(default_conf, update, mocker):
update.message.chat.id = 123
default_conf['telegram'] = {}
default_conf['telegram']['chat_id'] = 123
mocker.patch('telegram.update', MagicMock())
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock())
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) status_table = MagicMock()
mocker.patch.multiple('freqtrade.rpc',
send_msg=MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram', mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf, _CONF=default_conf,
init=MagicMock(), init=MagicMock(),
rpc_trade_status=MagicMock(return_value=(False, [1, 2, 3])),
_status_table=status_table,
send_msg=msg_mock) send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange', _status(bot=MagicMock(), update=update)
validate_pairs=MagicMock(), assert msg_mock.call_count == 3
get_ticker=ticker) update.message.text = MagicMock()
init(default_conf, create_engine('sqlite://')) update.message.text.replace = MagicMock(return_value='table 2 3')
_status(bot=MagicMock(), update=update)
assert status_table.call_count == 1