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:
parent
0a42a0e814
commit
9f6aedea47
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user