""" This module contains class to define a RPC communications """ import arrow from decimal import Decimal from datetime import datetime, timedelta from pandas import DataFrame import sqlalchemy as sql from freqtrade.logger import Logger from freqtrade.persistence import Trade from freqtrade.state import State from freqtrade import exchange from freqtrade.misc import shorten_date class RPC(object): """ RPC class can be used to have extra feature, like bot data, and access to DB data """ def __init__(self, freqtrade) -> None: """ Initializes all enabled rpc modules :param freqtrade: Instance of a freqtrade bot :return: None """ self.freqtrade = freqtrade self.logger = Logger( name=__name__, level=self.freqtrade.config.get('loglevel') ).get_logger() def rpc_trade_status(self) -> (bool, Trade): """ Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is a remotely exposed function :return: """ # Fetch open trade trades = Trade.query.filter(Trade.is_open.is_(True)).all() if self.freqtrade.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}`\n" \ "*Current Pair:* [{pair}]({market_url})\n" \ "*Open Since:* `{date}`\n" \ "*Amount:* `{amount}`\n" \ "*Open Rate:* `{open_rate:.8f}`\n" \ "*Close Rate:* `{close_rate}`\n" \ "*Current Rate:* `{current_rate:.8f}`\n" \ "*Close Profit:* `{close_profit}`\n" \ "*Current Profit:* `{current_profit:.2f}%`\n" \ "*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(self) -> (bool, DataFrame): trades = Trade.query.filter(Trade.is_open.is_(True)).all() if self.freqtrade.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(self, 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 = self.freqtrade.fiat_converter 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(self, 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 = self.freqtrade.fiat_converter # 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(self, 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 = self.freqtrade.fiat_converter symbol = fiat_display_currency value = fiat.convert_amount(total, 'BTC', symbol) return (False, (output, total, symbol, value)) def rpc_start(self) -> (bool, str): """ Handler for start. """ if self.freqtrade.get_state() == State.RUNNING: return (True, '*Status:* `already running`') else: self.freqtrade.update_state(State.RUNNING) return (False, '`Starting trader ...`') def rpc_stop(self) -> (bool, str): """ Handler for stop. """ if self.freqtrade.get_state() == State.RUNNING: self.freqtrade.update_state(State.STOPPED) return (False, '`Stopping trader ...`') else: return (True, '*Status:* `already stopped`') # FIX: no test for this!!!! def rpc_forcesell(self, trade_id) -> None: """ Handler for forcesell . 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'] self.freqtrade.execute_sell(trade, current_rate) # ---- EOF def _exec_forcesell ---- if self.freqtrade.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: self.logger.warning('forcesell: Invalid argument received') return (True, 'Invalid argument.') _exec_forcesell(trade) return (False, '') def rpc_performance(self) -> None: """ Handler for performance. Shows a performance statistic from finished trades """ if self.freqtrade.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(self) -> None: """ Returns the number of trades running :return: None """ if self.freqtrade.get_state() != State.RUNNING: return (True, '`trader is not running`') trades = Trade.query.filter(Trade.is_open.is_(True)).all() return (False, trades)