def test_convert_amount(mocker): mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0) fiat_convert = CryptoToFiatConverter() result = fiat_convert.convert_amount(crypto_amount=1.23, crypto_symbol="BTC", fiat_symbol="USD") assert result == 15184.35 result = fiat_convert.convert_amount(crypto_amount=1.23, crypto_symbol="BTC", fiat_symbol="BTC") assert result == 1.23
class RPC: """ RPC class can be used to have extra feature, like bot data, and access to DB data """ # Bind _fiat_converter if needed _fiat_converter: Optional[CryptoToFiatConverter] = None def __init__(self, freqtrade) -> None: """ Initializes all enabled rpc modules :param freqtrade: Instance of a freqtrade bot :return: None """ self._freqtrade = freqtrade self._config: Dict[str, Any] = freqtrade.config if self._config.get('fiat_display_currency', None): self._fiat_converter = CryptoToFiatConverter() @staticmethod def _rpc_show_config(config, botstate: Union[State, str], strategy_version: Optional[str] = None) -> Dict[str, Any]: """ Return a dict of config options. Explicitly does NOT return the full config to avoid leakage of sensitive information via rpc. """ val = { 'version': __version__, 'strategy_version': strategy_version, 'dry_run': config['dry_run'], 'stake_currency': config['stake_currency'], 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), 'stake_amount': config['stake_amount'], 'available_capital': config.get('available_capital'), 'max_open_trades': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), 'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {}, 'stoploss': config.get('stoploss'), 'trailing_stop': config.get('trailing_stop'), 'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), 'unfilledtimeout': config.get('unfilledtimeout'), 'use_custom_stoploss': config.get('use_custom_stoploss'), 'order_types': config.get('order_types'), 'bot_name': config.get('bot_name', 'freqtrade'), 'timeframe': config.get('timeframe'), 'timeframe_ms': timeframe_to_msecs(config['timeframe'] ) if 'timeframe' in config else 0, 'timeframe_min': timeframe_to_minutes(config['timeframe'] ) if 'timeframe' in config else 0, 'exchange': config['exchange']['name'], 'strategy': config['strategy'], 'forcebuy_enabled': config.get('forcebuy_enable', False), 'ask_strategy': config.get('ask_strategy', {}), 'bid_strategy': config.get('bid_strategy', {}), 'state': str(botstate), 'runmode': config['runmode'].value } return val def _rpc_trade_status(self, trade_ids: List[int] = []) -> List[Dict[str, Any]]: """ Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is a remotely exposed function """ # Fetch open trades if trade_ids: trades = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all() else: trades = Trade.get_open_trades() if not trades: raise RPCException('no active trade') else: results = [] for trade in trades: order = None if trade.open_order_id: order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) # calculate profit and send message to user if trade.is_open: try: current_rate = self._freqtrade.exchange.get_rate( trade.pair, refresh=False, side="sell") except (ExchangeError, PricingError): current_rate = NAN else: current_rate = trade.close_rate current_profit = trade.calc_profit_ratio(current_rate) current_profit_abs = trade.calc_profit(current_rate) current_profit_fiat: Optional[float] = None # Calculate fiat profit if self._fiat_converter: current_profit_fiat = self._fiat_converter.convert_amount( current_profit_abs, self._freqtrade.config['stake_currency'], self._freqtrade.config['fiat_display_currency'] ) # Calculate guaranteed profit (in case of trailing stop) stoploss_entry_dist = trade.calc_profit(trade.stop_loss) stoploss_entry_dist_ratio = trade.calc_profit_ratio(trade.stop_loss) # calculate distance to stoploss stoploss_current_dist = trade.stop_loss - current_rate stoploss_current_dist_ratio = stoploss_current_dist / current_rate trade_dict = trade.to_json() trade_dict.update(dict( base_currency=self._freqtrade.config['stake_currency'], close_profit=trade.close_profit if trade.close_profit is not None else None, current_rate=current_rate, current_profit=current_profit, # Deprecated current_profit_pct=round(current_profit * 100, 2), # Deprecated current_profit_abs=current_profit_abs, # Deprecated profit_ratio=current_profit, profit_pct=round(current_profit * 100, 2), profit_abs=current_profit_abs, profit_fiat=current_profit_fiat, stoploss_current_dist=stoploss_current_dist, stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2), stoploss_entry_dist=stoploss_entry_dist, stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8), open_order='({} {} rem={:.8f})'.format( order['type'], order['side'], order['remaining'] ) if order else None, )) results.append(trade_dict) return results def _rpc_status_table(self, stake_currency: str, fiat_display_currency: str) -> Tuple[List, List, float]: trades = Trade.get_open_trades() if not trades: raise RPCException('no active trade') else: trades_list = [] fiat_profit_sum = NAN for trade in trades: # calculate profit and send message to user try: current_rate = self._freqtrade.exchange.get_rate( trade.pair, refresh=False, side="sell") except (PricingError, ExchangeError): current_rate = NAN trade_profit = trade.calc_profit(current_rate) profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}' if self._fiat_converter: fiat_profit = self._fiat_converter.convert_amount( trade_profit, stake_currency, fiat_display_currency ) if fiat_profit and not isnan(fiat_profit): profit_str += f" ({fiat_profit:.2f})" fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \ else fiat_profit_sum + fiat_profit trades_list.append([ trade.id, trade.pair + ('*' if (trade.open_order_id is not None and trade.close_rate_requested is None) else '') + ('**' if (trade.close_rate_requested is not None) else ''), shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), profit_str ]) profitcol = "Profit" if self._fiat_converter: profitcol += " (" + fiat_display_currency + ")" columns = ['ID', 'Pair', 'Since', profitcol] return trades_list, columns, fiat_profit_sum def _rpc_daily_profit( self, timescale: int, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: today = datetime.now(timezone.utc).date() profit_days: Dict[date, Dict] = {} if not (isinstance(timescale, int) and timescale > 0): raise RPCException('timescale must be an integer greater than 0') for day in range(0, timescale): profitday = today - timedelta(days=day) trades = Trade.get_trades(trade_filter=[ Trade.is_open.is_(False), Trade.close_date >= profitday, Trade.close_date < (profitday + timedelta(days=1)) ]).order_by(Trade.close_date).all() curdayprofit = sum( trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) profit_days[profitday] = { 'amount': curdayprofit, 'trades': len(trades) } data = [ { 'date': key, 'abs_profit': value["amount"], 'fiat_value': self._fiat_converter.convert_amount( value['amount'], stake_currency, fiat_display_currency ) if self._fiat_converter else 0, 'trade_count': value["trades"], } for key, value in profit_days.items() ] return { 'stake_currency': stake_currency, 'fiat_display_currency': fiat_display_currency, 'data': data } def _rpc_weekly_profit( self, timescale: int, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: today = datetime.now(timezone.utc).date() first_iso_day_of_week = today - timedelta(days=today.weekday()) # Monday profit_weeks: Dict[date, Dict] = {} if not (isinstance(timescale, int) and timescale > 0): raise RPCException('timescale must be an integer greater than 0') for week in range(0, timescale): profitweek = first_iso_day_of_week - timedelta(weeks=week) trades = Trade.get_trades(trade_filter=[ Trade.is_open.is_(False), Trade.close_date >= profitweek, Trade.close_date < (profitweek + timedelta(weeks=1)) ]).order_by(Trade.close_date).all() curweekprofit = sum( trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) profit_weeks[profitweek] = { 'amount': curweekprofit, 'trades': len(trades) } data = [ { 'date': key, 'abs_profit': value["amount"], 'fiat_value': self._fiat_converter.convert_amount( value['amount'], stake_currency, fiat_display_currency ) if self._fiat_converter else 0, 'trade_count': value["trades"], } for key, value in profit_weeks.items() ] return { 'stake_currency': stake_currency, 'fiat_display_currency': fiat_display_currency, 'data': data } def _rpc_monthly_profit( self, timescale: int, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: first_day_of_month = datetime.now(timezone.utc).date().replace(day=1) profit_months: Dict[date, Dict] = {} if not (isinstance(timescale, int) and timescale > 0): raise RPCException('timescale must be an integer greater than 0') for month in range(0, timescale): profitmonth = first_day_of_month - relativedelta(months=month) trades = Trade.get_trades(trade_filter=[ Trade.is_open.is_(False), Trade.close_date >= profitmonth, Trade.close_date < (profitmonth + relativedelta(months=1)) ]).order_by(Trade.close_date).all() curmonthprofit = sum( trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) profit_months[profitmonth] = { 'amount': curmonthprofit, 'trades': len(trades) } data = [ { 'date': f"{key.year}-{key.month:02d}", 'abs_profit': value["amount"], 'fiat_value': self._fiat_converter.convert_amount( value['amount'], stake_currency, fiat_display_currency ) if self._fiat_converter else 0, 'trade_count': value["trades"], } for key, value in profit_months.items() ] return { 'stake_currency': stake_currency, 'fiat_display_currency': fiat_display_currency, 'data': data } def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict: """ Returns the X last trades """ order_by = Trade.id if order_by_id else Trade.close_date.desc() if limit: trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( order_by).limit(limit).offset(offset) else: trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( Trade.close_date.desc()).all() output = [trade.to_json() for trade in trades] return { "trades": output, "trades_count": len(output), "total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(), } def _rpc_stats(self) -> Dict[str, Any]: """ Generate generic stats for trades in database """ def trade_win_loss(trade): if trade.close_profit > 0: return 'wins' elif trade.close_profit < 0: return 'losses' else: return 'draws' trades = trades = Trade.get_trades([Trade.is_open.is_(False)]) # Sell reason sell_reasons = {} for trade in trades: if trade.sell_reason not in sell_reasons: sell_reasons[trade.sell_reason] = {'wins': 0, 'losses': 0, 'draws': 0} sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1 # Duration dur: Dict[str, List[int]] = {'wins': [], 'draws': [], 'losses': []} for trade in trades: if trade.close_date is not None and trade.open_date is not None: trade_dur = (trade.close_date - trade.open_date).total_seconds() dur[trade_win_loss(trade)].append(trade_dur) wins_dur = sum(dur['wins']) / len(dur['wins']) if len(dur['wins']) > 0 else 'N/A' draws_dur = sum(dur['draws']) / len(dur['draws']) if len(dur['draws']) > 0 else 'N/A' losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else 'N/A' durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur} return {'sell_reasons': sell_reasons, 'durations': durations} def _rpc_trade_statistics( self, stake_currency: str, fiat_display_currency: str, start_date: datetime = datetime.fromtimestamp(0)) -> Dict[str, Any]: """ Returns cumulative profit statistics """ trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) | Trade.is_open.is_(True)) trades = Trade.get_trades(trade_filter).order_by(Trade.id).all() profit_all_coin = [] profit_all_ratio = [] profit_closed_coin = [] profit_closed_ratio = [] durations = [] winning_trades = 0 losing_trades = 0 for trade in trades: current_rate: float = 0.0 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_ratio = trade.close_profit profit_closed_coin.append(trade.close_profit_abs) profit_closed_ratio.append(profit_ratio) if trade.close_profit >= 0: winning_trades += 1 else: losing_trades += 1 else: # Get current rate try: current_rate = self._freqtrade.exchange.get_rate( trade.pair, refresh=False, side="sell") except (PricingError, ExchangeError): current_rate = NAN profit_ratio = trade.calc_profit_ratio(rate=current_rate) profit_all_coin.append( trade.calc_profit(rate=trade.close_rate or current_rate) ) profit_all_ratio.append(profit_ratio) best_pair = Trade.get_best_pair(start_date) # Prepare data to display profit_closed_coin_sum = round(sum(profit_closed_coin), 8) profit_closed_ratio_mean = float(mean(profit_closed_ratio) if profit_closed_ratio else 0.0) profit_closed_ratio_sum = sum(profit_closed_ratio) if profit_closed_ratio else 0.0 profit_closed_fiat = self._fiat_converter.convert_amount( profit_closed_coin_sum, stake_currency, fiat_display_currency ) if self._fiat_converter else 0 profit_all_coin_sum = round(sum(profit_all_coin), 8) profit_all_ratio_mean = float(mean(profit_all_ratio) if profit_all_ratio else 0.0) # Doing the sum is not right - overall profit needs to be based on initial capital profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0 starting_balance = self._freqtrade.wallets.get_starting_balance() profit_closed_ratio_fromstart = 0 profit_all_ratio_fromstart = 0 if starting_balance: profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance profit_all_fiat = self._fiat_converter.convert_amount( profit_all_coin_sum, stake_currency, fiat_display_currency ) if self._fiat_converter else 0 first_date = trades[0].open_date if trades else None last_date = trades[-1].open_date if trades else None num = float(len(durations) or 1) return { 'profit_closed_coin': profit_closed_coin_sum, 'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2), 'profit_closed_ratio_mean': profit_closed_ratio_mean, 'profit_closed_percent_sum': round(profit_closed_ratio_sum * 100, 2), 'profit_closed_ratio_sum': profit_closed_ratio_sum, 'profit_closed_ratio': profit_closed_ratio_fromstart, 'profit_closed_percent': round(profit_closed_ratio_fromstart * 100, 2), 'profit_closed_fiat': profit_closed_fiat, 'profit_all_coin': profit_all_coin_sum, 'profit_all_percent_mean': round(profit_all_ratio_mean * 100, 2), 'profit_all_ratio_mean': profit_all_ratio_mean, 'profit_all_percent_sum': round(profit_all_ratio_sum * 100, 2), 'profit_all_ratio_sum': profit_all_ratio_sum, 'profit_all_ratio': profit_all_ratio_fromstart, 'profit_all_percent': round(profit_all_ratio_fromstart * 100, 2), 'profit_all_fiat': profit_all_fiat, 'trade_count': len(trades), 'closed_trade_count': len([t for t in trades if not t.is_open]), 'first_trade_date': arrow.get(first_date).humanize() if first_date else '', 'first_trade_timestamp': int(first_date.timestamp() * 1000) if first_date else 0, 'latest_trade_date': arrow.get(last_date).humanize() if last_date else '', 'latest_trade_timestamp': int(last_date.timestamp() * 1000) if last_date else 0, 'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0], 'best_pair': best_pair[0] if best_pair else '', 'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0, # Deprecated 'best_pair_profit_ratio': best_pair[1] if best_pair else 0, 'winning_trades': winning_trades, 'losing_trades': losing_trades, } def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: """ Returns current account balance per crypto """ output = [] total = 0.0 try: tickers = self._freqtrade.exchange.get_tickers(cached=True) except (ExchangeError): raise RPCException('Error getting current tickers.') self._freqtrade.wallets.update(require_update=False) starting_capital = self._freqtrade.wallets.get_starting_balance() starting_cap_fiat = self._fiat_converter.convert_amount( starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0 for coin, balance in self._freqtrade.wallets.get_all_balances().items(): if not balance.total: continue est_stake: float = 0 if coin == stake_currency: rate = 1.0 est_stake = balance.total else: try: pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) rate = tickers.get(pair, {}).get('bid', None) if rate: if pair.startswith(stake_currency) and not pair.endswith(stake_currency): rate = 1.0 / rate est_stake = rate * balance.total except (ExchangeError): logger.warning(f" Could not get rate for pair {coin}.") continue total = total + (est_stake or 0) output.append({ 'currency': coin, 'free': balance.free if balance.free is not None else 0, 'balance': balance.total if balance.total is not None else 0, 'used': balance.used if balance.used is not None else 0, 'est_stake': est_stake or 0, 'stake': stake_currency, }) if total == 0.0: if self._freqtrade.config['dry_run']: raise RPCException('Running in Dry Run, balances are not available.') else: raise RPCException('All balances are zero.') value = self._fiat_converter.convert_amount( total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 starting_capital_ratio = 0.0 starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0 starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0 return { 'currencies': output, 'total': total, 'symbol': fiat_display_currency, 'value': value, 'stake': stake_currency, 'starting_capital': starting_capital, 'starting_capital_ratio': starting_capital_ratio, 'starting_capital_pct': round(starting_capital_ratio * 100, 2), 'starting_capital_fiat': starting_cap_fiat, 'starting_capital_fiat_ratio': starting_cap_fiat_ratio, 'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2), 'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else '' } def _rpc_start(self) -> Dict[str, str]: """ Handler for start """ if self._freqtrade.state == State.RUNNING: return {'status': 'already running'} self._freqtrade.state = State.RUNNING return {'status': 'starting trader ...'} def _rpc_stop(self) -> Dict[str, str]: """ Handler for stop """ if self._freqtrade.state == State.RUNNING: self._freqtrade.state = State.STOPPED return {'status': 'stopping trader ...'} return {'status': 'already stopped'} def _rpc_reload_config(self) -> Dict[str, str]: """ Handler for reload_config. """ self._freqtrade.state = State.RELOAD_CONFIG return {'status': 'Reloading config ...'} def _rpc_stopbuy(self) -> Dict[str, str]: """ Handler to stop buying, but handle open trades gracefully. """ if self._freqtrade.state == State.RUNNING: # Set 'max_open_trades' to 0 self._freqtrade.config['max_open_trades'] = 0 return {'status': 'No more buy will occur from now. Run /reload_config to reset.'} def _rpc_forcesell(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]: """ Handler for forcesell <id>. Sells the given trade at current price """ def _exec_forcesell(trade: Trade) -> None: # Check if there is there is an open order fully_canceled = False if trade.open_order_id: order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) if order['side'] == 'buy': fully_canceled = self._freqtrade.handle_cancel_enter( trade, order, CANCEL_REASON['FORCE_SELL']) if order['side'] == 'sell': # Cancel order - so it is placed anew with a fresh price. self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL']) if not fully_canceled: # Get current rate and execute sell current_rate = self._freqtrade.exchange.get_rate( trade.pair, refresh=False, side="sell") sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) order_type = ordertype or self._freqtrade.strategy.order_types.get( "forcesell", self._freqtrade.strategy.order_types["sell"]) self._freqtrade.execute_trade_exit( trade, current_rate, sell_reason, ordertype=order_type) # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: raise RPCException('trader is not running') with self._freqtrade._exit_lock: if trade_id == 'all': # Execute sell for all open orders for trade in Trade.get_open_trades(): _exec_forcesell(trade) Trade.commit() self._freqtrade.wallets.update() return {'result': 'Created sell orders for all open trades.'} # Query for trade trade = Trade.get_trades( trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ] ).first() if not trade: logger.warning('forcesell: Invalid argument received') raise RPCException('invalid argument') _exec_forcesell(trade) Trade.commit() self._freqtrade.wallets.update() return {'result': f'Created sell order for trade {trade_id}.'} def _rpc_forcebuy(self, pair: str, price: Optional[float], order_type: Optional[str] = None) -> Optional[Trade]: """ Handler for forcebuy <asset> <price> Buys a pair trade at the given or current price """ if not self._freqtrade.config.get('forcebuy_enable', False): raise RPCException('Forcebuy not enabled.') if self._freqtrade.state != State.RUNNING: raise RPCException('trader is not running') # Check if pair quote currency equals to the stake currency. stake_currency = self._freqtrade.config.get('stake_currency') if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency: raise RPCException( f'Wrong pair selected. Only pairs with stake-currency {stake_currency} allowed.') # check if valid pair # check if pair already has an open pair trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() if trade: raise RPCException(f'position for {pair} already open - id: {trade.id}') # gen stake amount stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair) # execute buy if not order_type: order_type = self._freqtrade.strategy.order_types.get( 'forcebuy', self._freqtrade.strategy.order_types['buy']) if self._freqtrade.execute_entry(pair, stakeamount, price, ordertype=order_type): Trade.commit() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() return trade else: return None def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]: """ Handler for delete <id>. Delete the given trade and close eventually existing open orders. """ with self._freqtrade._exit_lock: c_count = 0 trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first() if not trade: logger.warning('delete trade: Invalid argument received') raise RPCException('invalid argument') # Try cancelling regular order if that exists if trade.open_order_id: try: self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair) c_count += 1 except (ExchangeError): pass # cancel stoploss on exchange ... if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id): try: self._freqtrade.exchange.cancel_stoploss_order(trade.stoploss_order_id, trade.pair) c_count += 1 except (ExchangeError): pass trade.delete() self._freqtrade.wallets.update() return { 'result': 'success', 'trade_id': trade_id, 'result_msg': f'Deleted trade {trade_id}. Closed {c_count} open orders.', 'cancel_order_count': c_count, } def _rpc_performance(self) -> List[Dict[str, Any]]: """ Handler for performance. Shows a performance statistic from finished trades """ pair_rates = Trade.get_overall_performance() return pair_rates def _rpc_buy_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]: """ Handler for buy tag performance. Shows a performance statistic from finished trades """ buy_tags = Trade.get_buy_tag_performance(pair) return buy_tags def _rpc_sell_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]: """ Handler for sell reason performance. Shows a performance statistic from finished trades """ sell_reasons = Trade.get_sell_reason_performance(pair) return sell_reasons def _rpc_mix_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]: """ Handler for mix tag (buy_tag + sell_reason) performance. Shows a performance statistic from finished trades """ mix_tags = Trade.get_mix_tag_performance(pair) return mix_tags def _rpc_count(self) -> Dict[str, float]: """ Returns the number of trades running """ if self._freqtrade.state != State.RUNNING: raise RPCException('trader is not running') trades = Trade.get_open_trades() return { 'current': len(trades), 'max': (int(self._freqtrade.config['max_open_trades']) if self._freqtrade.config['max_open_trades'] != float('inf') else -1), 'total_stake': sum((trade.open_rate * trade.amount) for trade in trades) } def _rpc_locks(self) -> Dict[str, Any]: """ Returns the current locks """ locks = PairLocks.get_pair_locks(None) return { 'lock_count': len(locks), 'locks': [lock.to_json() for lock in locks] } def _rpc_delete_lock(self, lockid: Optional[int] = None, pair: Optional[str] = None) -> Dict[str, Any]: """ Delete specific lock(s) """ locks = [] if pair: locks = PairLocks.get_pair_locks(pair) if lockid: locks = PairLock.query.filter(PairLock.id == lockid).all() for lock in locks: lock.active = False lock.lock_end_time = datetime.now(timezone.utc) PairLock.query.session.commit() return self._rpc_locks() def _rpc_whitelist(self) -> Dict: """ Returns the currently active whitelist""" res = {'method': self._freqtrade.pairlists.name_list, 'length': len(self._freqtrade.active_pair_whitelist), 'whitelist': self._freqtrade.active_pair_whitelist } return res def _rpc_blacklist(self, add: List[str] = None) -> Dict: """ Returns the currently active blacklist""" errors = {} if add: for pair in add: if pair not in self._freqtrade.pairlists.blacklist: try: expand_pairlist([pair], self._freqtrade.exchange.get_markets().keys()) self._freqtrade.pairlists.blacklist.append(pair) except ValueError: errors[pair] = { 'error_msg': f'Pair {pair} is not a valid wildcard.'} else: errors[pair] = { 'error_msg': f'Pair {pair} already in pairlist.'} res = {'method': self._freqtrade.pairlists.name_list, 'length': len(self._freqtrade.pairlists.blacklist), 'blacklist': self._freqtrade.pairlists.blacklist, 'blacklist_expanded': self._freqtrade.pairlists.expanded_blacklist, 'errors': errors, } return res @staticmethod def _rpc_get_logs(limit: Optional[int]) -> Dict[str, Any]: """Returns the last X logs""" if limit: buffer = bufferHandler.buffer[-limit:] else: buffer = bufferHandler.buffer records = [[datetime.fromtimestamp(r.created).strftime(DATETIME_PRINT_FORMAT), r.created * 1000, r.name, r.levelname, r.message + ('\n' + r.exc_text if r.exc_text else '')] for r in buffer] # Log format: # [logtime-formatted, logepoch, logger-name, loglevel, message \n + exception] # e.g. ["2020-08-27 11:35:01", 1598520901097.9397, # "freqtrade.worker", "INFO", "Starting worker develop"] return {'log_count': len(records), 'logs': records} def _rpc_edge(self) -> List[Dict[str, Any]]: """ Returns information related to Edge """ if not self._freqtrade.edge: raise RPCException('Edge is not enabled.') return self._freqtrade.edge.accepted_pairs() @staticmethod def _convert_dataframe_to_dict(strategy: str, pair: str, timeframe: str, dataframe: DataFrame, last_analyzed: datetime) -> Dict[str, Any]: has_content = len(dataframe) != 0 buy_signals = 0 sell_signals = 0 if has_content: dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000 # Move signal close to separate column when signal for easy plotting if 'buy' in dataframe.columns: buy_mask = (dataframe['buy'] == 1) buy_signals = int(buy_mask.sum()) dataframe.loc[buy_mask, '_buy_signal_close'] = dataframe.loc[buy_mask, 'close'] if 'sell' in dataframe.columns: sell_mask = (dataframe['sell'] == 1) sell_signals = int(sell_mask.sum()) dataframe.loc[sell_mask, '_sell_signal_close'] = dataframe.loc[sell_mask, 'close'] dataframe = dataframe.replace([inf, -inf], NAN) dataframe = dataframe.replace({NAN: None}) res = { 'pair': pair, 'timeframe': timeframe, 'timeframe_ms': timeframe_to_msecs(timeframe), 'strategy': strategy, 'columns': list(dataframe.columns), 'data': dataframe.values.tolist(), 'length': len(dataframe), 'buy_signals': buy_signals, 'sell_signals': sell_signals, 'last_analyzed': last_analyzed, 'last_analyzed_ts': int(last_analyzed.timestamp()), 'data_start': '', 'data_start_ts': 0, 'data_stop': '', 'data_stop_ts': 0, } if has_content: res.update({ 'data_start': str(dataframe.iloc[0]['date']), 'data_start_ts': int(dataframe.iloc[0]['__date_ts']), 'data_stop': str(dataframe.iloc[-1]['date']), 'data_stop_ts': int(dataframe.iloc[-1]['__date_ts']), }) return res def _rpc_analysed_dataframe(self, pair: str, timeframe: str, limit: Optional[int]) -> Dict[str, Any]: _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe( pair, timeframe) _data = _data.copy() if limit: _data = _data.iloc[-limit:] return self._convert_dataframe_to_dict(self._freqtrade.config['strategy'], pair, timeframe, _data, last_analyzed) @staticmethod def _rpc_analysed_history_full(config, pair: str, timeframe: str, timerange: str) -> Dict[str, Any]: timerange_parsed = TimeRange.parse_timerange(timerange) _data = load_data( datadir=config.get("datadir"), pairs=[pair], timeframe=timeframe, timerange=timerange_parsed, data_format=config.get('dataformat_ohlcv', 'json'), ) if pair not in _data: raise RPCException(f"No data for {pair}, {timeframe} in {timerange} found.") from freqtrade.data.dataprovider import DataProvider from freqtrade.resolvers.strategy_resolver import StrategyResolver strategy = StrategyResolver.load_strategy(config) strategy.dp = DataProvider(config, exchange=None, pairlists=None) df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) return RPC._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe, df_analyzed, arrow.Arrow.utcnow().datetime) def _rpc_plot_config(self) -> Dict[str, Any]: if (self._freqtrade.strategy.plot_config and 'subplots' not in self._freqtrade.strategy.plot_config): self._freqtrade.strategy.plot_config['subplots'] = {} return self._freqtrade.strategy.plot_config @staticmethod def _rpc_sysinfo() -> Dict[str, Any]: return { "cpu_pct": psutil.cpu_percent(interval=1, percpu=True), "ram_pct": psutil.virtual_memory().percent }
class Telegram(RPC): """ This class handles all telegram communication """ def __init__(self, freqtrade) -> None: """ Init the Telegram call, and init the super class RPC :param freqtrade: Instance of a freqtrade bot :return: None """ super().__init__(freqtrade) self._updater: Updater = None self._config = freqtrade.config self._init() if self._config.get('fiat_display_currency', None): self._fiat_converter = CryptoToFiatConverter() def _init(self) -> None: """ Initializes this module with the given config, registers all known command handlers and starts polling for message updates """ self._updater = Updater(token=self._config['telegram']['token'], workers=0) # Register command handler and start telegram message polling handles = [ CommandHandler('status', self._status), CommandHandler('profit', self._profit), CommandHandler('balance', self._balance), CommandHandler('start', self._start), CommandHandler('stop', self._stop), CommandHandler('forcesell', self._forcesell), CommandHandler('forcebuy', self._forcebuy), CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), CommandHandler('reload_conf', self._reload_conf), CommandHandler('whitelist', self._whitelist), CommandHandler('help', self._help), CommandHandler('version', self._version), ] for handle in handles: self._updater.dispatcher.add_handler(handle) self._updater.start_polling( clean=True, bootstrap_retries=-1, timeout=30, read_latency=60, ) logger.info('rpc.telegram is listening for following commands: %s', [h.command for h in handles]) def cleanup(self) -> None: """ Stops all running telegram threads. :return: None """ self._updater.stop() def send_msg(self, msg: Dict[str, Any]) -> None: """ Send a message to telegram channel """ if msg['type'] == RPCMessageType.BUY_NOTIFICATION: if self._fiat_converter: msg['stake_amount_fiat'] = self._fiat_converter.convert_amount( msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) else: msg['stake_amount_fiat'] = 0 message = ("*{exchange}:* Buying [{pair}]({market_url})\n" "with limit `{limit:.8f}\n" "({stake_amount:.6f} {stake_currency}").format(**msg) if msg.get('fiat_currency', None): message += ",{stake_amount_fiat:.3f} {fiat_currency}".format( **msg) message += ")`" elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: msg['amount'] = round(msg['amount'], 8) msg['profit_percent'] = round(msg['profit_percent'] * 100, 2) message = ("*{exchange}:* Selling [{pair}]({market_url})\n" "*Limit:* `{limit:.8f}`\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" "*Sell Reason:* `{sell_reason}`\n" "*Profit:* `{profit_percent:.2f}%`").format(**msg) # Check if all sell properties are available. # This might not be the case if the message origin is triggered by /forcesell if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) and self._fiat_converter): msg['profit_fiat'] = self._fiat_converter.convert_amount( msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) message += ('` ({gain}: {profit_amount:.8f} {stake_currency}`' '` / {profit_fiat:.3f} {fiat_currency})`').format( **msg) elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: message = '*Status:* `{status}`'.format(**msg) elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION: message = '*Warning:* `{status}`'.format(**msg) elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION: message = '{status}'.format(**msg) else: raise NotImplementedError('Unknown message type: {}'.format( msg['type'])) self._send_msg(message) @authorized_only def _status(self, bot: Bot, update: Update) -> None: """ Handler for /status. Returns the current TradeThread status :param bot: telegram bot :param update: message update :return: None """ # Check if additional parameters are passed params = update.message.text.replace('/status', '').split(' ') \ if update.message.text else [] if 'table' in params: self._status_table(bot, update) return try: results = self._rpc_trade_status() # pre format data for result in results: result['date'] = result['date'].humanize() messages = [ "*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(**result) for result in results ] for msg in messages: self._send_msg(msg, bot=bot) except RPCException as e: self._send_msg(str(e), bot=bot) @authorized_only def _status_table(self, bot: Bot, update: Update) -> None: """ Handler for /status table. Returns the current TradeThread status in table format :param bot: telegram bot :param update: message update :return: None """ try: df_statuses = self._rpc_status_table() message = tabulate(df_statuses, headers='keys', tablefmt='simple') self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e), bot=bot) @authorized_only def _daily(self, bot: Bot, update: Update) -> None: """ Handler for /daily <n> Returns a daily profit (in BTC) over the last n days. :param bot: telegram bot :param update: message update :return: None """ stake_cur = self._config['stake_currency'] fiat_disp_cur = self._config.get('fiat_display_currency', '') try: timescale = int(update.message.text.replace('/daily', '').strip()) except (TypeError, ValueError): timescale = 7 try: stats = self._rpc_daily_profit(timescale, stake_cur, fiat_disp_cur) stats_tab = tabulate(stats, headers=[ 'Day', f'Profit {stake_cur}', f'Profit {fiat_disp_cur}' ], tablefmt='simple') message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>' self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e), bot=bot) @authorized_only def _profit(self, bot: Bot, update: Update) -> None: """ Handler for /profit. Returns a cumulative profit statistics. :param bot: telegram bot :param update: message update :return: None """ stake_cur = self._config['stake_currency'] fiat_disp_cur = self._config.get('fiat_display_currency', '') try: stats = self._rpc_trade_statistics(stake_cur, fiat_disp_cur) profit_closed_coin = stats['profit_closed_coin'] profit_closed_percent = stats['profit_closed_percent'] profit_closed_fiat = stats['profit_closed_fiat'] profit_all_coin = stats['profit_all_coin'] profit_all_percent = stats['profit_all_percent'] profit_all_fiat = stats['profit_all_fiat'] trade_count = stats['trade_count'] first_trade_date = stats['first_trade_date'] latest_trade_date = stats['latest_trade_date'] avg_duration = stats['avg_duration'] best_pair = stats['best_pair'] best_rate = stats['best_rate'] # Message to display markdown_msg = "*ROI:* Close trades\n" \ f"∙ `{profit_closed_coin:.8f} {stake_cur} "\ f"({profit_closed_percent:.2f}%)`\n" \ f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n" \ f"*ROI:* All trades\n" \ f"∙ `{profit_all_coin:.8f} {stake_cur} ({profit_all_percent:.2f}%)`\n" \ f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" \ f"*Total Trade Count:* `{trade_count}`\n" \ f"*First Trade opened:* `{first_trade_date}`\n" \ f"*Latest Trade opened:* `{latest_trade_date}`\n" \ f"*Avg. Duration:* `{avg_duration}`\n" \ f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`" self._send_msg(markdown_msg, bot=bot) except RPCException as e: self._send_msg(str(e), bot=bot) @authorized_only def _balance(self, bot: Bot, update: Update) -> None: """ Handler for /balance """ try: result = self._rpc_balance( self._config.get('fiat_display_currency', '')) output = '' for currency in result['currencies']: if currency['est_btc'] > 0.0001: output += "*{currency}:*\n" \ "\t`Available: {available: .8f}`\n" \ "\t`Balance: {balance: .8f}`\n" \ "\t`Pending: {pending: .8f}`\n" \ "\t`Est. BTC: {est_btc: .8f}`\n".format(**currency) else: output += "*{currency}:* not showing <1$ amount \n".format( **currency) output += "\n*Estimated Value*:\n" \ "\t`BTC: {total: .8f}`\n" \ "\t`{symbol}: {value: .2f}`\n".format(**result) self._send_msg(output, bot=bot) except RPCException as e: self._send_msg(str(e), bot=bot) @authorized_only def _start(self, bot: Bot, update: Update) -> None: """ Handler for /start. Starts TradeThread :param bot: telegram bot :param update: message update :return: None """ msg = self._rpc_start() self._send_msg('Status: `{status}`'.format(**msg), bot=bot) @authorized_only def _stop(self, bot: Bot, update: Update) -> None: """ Handler for /stop. Stops TradeThread :param bot: telegram bot :param update: message update :return: None """ msg = self._rpc_stop() self._send_msg('Status: `{status}`'.format(**msg), bot=bot) @authorized_only def _reload_conf(self, bot: Bot, update: Update) -> None: """ Handler for /reload_conf. Triggers a config file reload :param bot: telegram bot :param update: message update :return: None """ msg = self._rpc_reload_conf() self._send_msg('Status: `{status}`'.format(**msg), bot=bot) @authorized_only def _forcesell(self, bot: Bot, update: Update) -> None: """ Handler for /forcesell <id>. Sells the given trade at current price :param bot: telegram bot :param update: message update :return: None """ trade_id = update.message.text.replace('/forcesell', '').strip() try: self._rpc_forcesell(trade_id) except RPCException as e: self._send_msg(str(e), bot=bot) @authorized_only def _forcebuy(self, bot: Bot, update: Update) -> None: """ Handler for /forcebuy <asset> <price>. Buys a pair trade at the given or current price :param bot: telegram bot :param update: message update :return: None """ message = update.message.text.replace('/forcebuy', '').strip().split() pair = message[0] price = float(message[1]) if len(message) > 1 else None try: self._rpc_forcebuy(pair, price) except RPCException as e: self._send_msg(str(e), bot=bot) @authorized_only def _performance(self, bot: Bot, update: Update) -> None: """ Handler for /performance. Shows a performance statistic from finished trades :param bot: telegram bot :param update: message update :return: None """ try: trades = self._rpc_performance() stats = '\n'.join( '{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'. format(index=i + 1, pair=trade['pair'], profit=trade['profit'], count=trade['count']) for i, trade in enumerate(trades)) message = '<b>Performance:</b>\n{}'.format(stats) self._send_msg(message, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e), bot=bot) @authorized_only def _count(self, bot: Bot, update: Update) -> None: """ Handler for /count. Returns the number of trades running :param bot: telegram bot :param update: message update :return: None """ try: trades = self._rpc_count() message = tabulate( { 'current': [len(trades)], 'max': [self._config['max_open_trades']], 'total stake': [ sum((trade.open_rate * trade.amount) for trade in trades) ] }, headers=['current', 'max', 'total stake'], tablefmt='simple') message = "<pre>{}</pre>".format(message) logger.debug(message) self._send_msg(message, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e), bot=bot) @authorized_only def _whitelist(self, bot: Bot, update: Update) -> None: """ Handler for /whitelist Shows the currently active whitelist """ try: whitelist = self._rpc_whitelist() message = f"Using whitelist `{whitelist['method']}` with {whitelist['length']} pairs\n" message += f"`{', '.join(whitelist['whitelist'])}`" logger.debug(message) self._send_msg(message) except RPCException as e: self._send_msg(str(e), bot=bot) @authorized_only def _help(self, bot: Bot, update: Update) -> None: """ Handler for /help. Show commands of the bot :param bot: telegram bot :param update: message update :return: None """ message = "*/start:* `Starts the trader`\n" \ "*/stop:* `Stops the trader`\n" \ "*/status [table]:* `Lists all open trades`\n" \ " *table :* `will display trades in a table`\n" \ "*/profit:* `Lists cumulative profit from all finished trades`\n" \ "*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, " \ "regardless of profit`\n" \ "*/performance:* `Show performance of each finished trade grouped by pair`\n" \ "*/daily <n>:* `Shows profit or loss per day, over the last n days`\n" \ "*/count:* `Show number of trades running compared to allowed number of trades`" \ "\n" \ "*/balance:* `Show account balance per currency`\n" \ "*/reload_conf:* `Reload configuration file` \n" \ "*/whitelist:* `Show current whitelist` \n" \ "*/help:* `This help message`\n" \ "*/version:* `Show version`" self._send_msg(message, bot=bot) @authorized_only def _version(self, bot: Bot, update: Update) -> None: """ Handler for /version. Show version information :param bot: telegram bot :param update: message update :return: None """ self._send_msg('*Version:* `{}`'.format(__version__), bot=bot) def _send_msg(self, msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: """ Send given markdown message :param msg: message :param bot: alternative bot :param parse_mode: telegram parse mode :return: None """ bot = bot or self._updater.bot keyboard = [['/daily', '/profit', '/balance'], ['/status', '/status table', '/performance'], ['/count', '/start', '/stop', '/help']] reply_markup = ReplyKeyboardMarkup(keyboard) try: try: bot.send_message(self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, reply_markup=reply_markup) except NetworkError as network_err: # Sometimes the telegram server resets the current connection, # if this is the case we send the message again. logger.warning( 'Telegram NetworkError: %s! Trying one more time.', network_err.message) bot.send_message(self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, reply_markup=reply_markup) except TelegramError as telegram_err: logger.warning('TelegramError: %s! Giving up on that message.', telegram_err.message)
class Telegram(RPC): """ This class handles all telegram communication """ def __init__(self, freqtrade) -> None: """ Init the Telegram call, and init the super class RPC :param freqtrade: Instance of a freqtrade bot :return: None """ super().__init__(freqtrade) self._updater: Updater = None self._config = freqtrade.config self._init() if self._config.get('fiat_display_currency', None): self._fiat_converter = CryptoToFiatConverter() def _init(self) -> None: """ Initializes this module with the given config, registers all known command handlers and starts polling for message updates """ self._updater = Updater(token=self._config['telegram']['token'], workers=0, use_context=True) # Register command handler and start telegram message polling handles = [ CommandHandler('status', self._status), CommandHandler('profit', self._profit), CommandHandler('balance', self._balance), CommandHandler('start', self._start), CommandHandler('stop', self._stop), CommandHandler('forcesell', self._forcesell), CommandHandler('forcebuy', self._forcebuy), CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), CommandHandler('reload_conf', self._reload_conf), CommandHandler('stopbuy', self._stopbuy), CommandHandler('whitelist', self._whitelist), CommandHandler('blacklist', self._blacklist), CommandHandler('edge', self._edge), CommandHandler('help', self._help), CommandHandler('version', self._version), ] for handle in handles: self._updater.dispatcher.add_handler(handle) self._updater.start_polling( clean=True, bootstrap_retries=-1, timeout=30, read_latency=60, ) logger.info( 'rpc.telegram is listening for following commands: %s', [h.command for h in handles] ) def cleanup(self) -> None: """ Stops all running telegram threads. :return: None """ self._updater.stop() def send_msg(self, msg: Dict[str, Any]) -> None: """ Send a message to telegram channel """ if msg['type'] == RPCMessageType.BUY_NOTIFICATION: if self._fiat_converter: msg['stake_amount_fiat'] = self._fiat_converter.convert_amount( msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) else: msg['stake_amount_fiat'] = 0 message = ("*{exchange}:* Buying {pair}\n" "at rate `{limit:.8f}\n" "({stake_amount:.6f} {stake_currency}").format(**msg) if msg.get('fiat_currency', None): message += ",{stake_amount_fiat:.3f} {fiat_currency}".format(**msg) message += ")`" elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: msg['amount'] = round(msg['amount'], 8) msg['profit_percent'] = round(msg['profit_percent'] * 100, 2) message = ("*{exchange}:* Selling {pair}\n" "*Rate:* `{limit:.8f}`\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" "*Sell Reason:* `{sell_reason}`\n" "*Profit:* `{profit_percent:.2f}%`").format(**msg) # Check if all sell properties are available. # This might not be the case if the message origin is triggered by /forcesell if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) and self._fiat_converter): msg['profit_fiat'] = self._fiat_converter.convert_amount( msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) message += ('` ({gain}: {profit_amount:.8f} {stake_currency}`' '` / {profit_fiat:.3f} {fiat_currency})`').format(**msg) elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: message = '*Status:* `{status}`'.format(**msg) elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION: message = '*Warning:* `{status}`'.format(**msg) elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION: message = '{status}'.format(**msg) else: raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) self._send_msg(message) @authorized_only def _status(self, update: Update, context: CallbackContext) -> None: """ Handler for /status. Returns the current TradeThread status :param bot: telegram bot :param update: message update :return: None """ if 'table' in context.args: self._status_table(update, context) return try: results = self._rpc_trade_status() messages = [] for r in results: lines = [ "*ID сделки:* `{trade_id}` `(since {open_date_hum})`", "*Выбранная пара:* {pair}", "*Количество:* `{amount} ({stake_amount} {base_currency})`", "*Открытый рейт:* `{open_rate:.8f}`", "*Закрытый рейт:* `{close_rate}`" if r['close_rate'] else "", "*Настоящий рейт:* `{current_rate:.8f}`", "*Профит на выходе:* `{close_profit}`" if r['close_profit'] else "", "*Профит сейчас:* `{current_profit:.2f}%`", # Adding initial stoploss only if it is different from stoploss "*Initial Stoploss:* `{initial_stop_loss:.8f}` " + ("`({initial_stop_loss_pct:.2f}%)`" if r['initial_stop_loss_pct'] else "") if r['stop_loss'] != r['initial_stop_loss'] else "", # Adding stoploss and stoploss percentage only if it is not None "*Stoploss:* `{stop_loss:.8f}` " + ("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""), "*Open Order:* `{open_order}`" if r['open_order'] else "" ] # Filter empty lines using list-comprehension messages.append("\n".join([l for l in lines if l]).format(**r)) for msg in messages: self._send_msg(msg) except RPCException as e: self._send_msg(str(e)) @authorized_only def _status_table(self, update: Update, context: CallbackContext) -> None: """ Handler for /status table. Returns the current TradeThread status in table format :param bot: telegram bot :param update: message update :return: None """ try: statlist, head = self._rpc_status_table(self._config['stake_currency'], self._config.get('fiat_display_currency', '')) message = tabulate(statlist, headers=head, tablefmt='simple') self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) @authorized_only def _daily(self, update: Update, context: CallbackContext) -> None: """ Handler for /daily <n> Returns a daily profit (in BTC) over the last n days. :param bot: telegram bot :param update: message update :return: None """ stake_cur = self._config['stake_currency'] fiat_disp_cur = self._config.get('fiat_display_currency', '') try: timescale = int(context.args[0]) except (TypeError, ValueError, IndexError): timescale = 7 try: stats = self._rpc_daily_profit( timescale, stake_cur, fiat_disp_cur ) stats_tab = tabulate(stats, headers=[ 'Day', f'Profit {stake_cur}', f'Profit {fiat_disp_cur}', f'Trades' ], tablefmt='simple') message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>' self._send_msg(message, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) @authorized_only def _profit(self, update: Update, context: CallbackContext) -> None: """ Handler for /profit. Returns a cumulative profit statistics. :param bot: telegram bot :param update: message update :return: None """ stake_cur = self._config['stake_currency'] fiat_disp_cur = self._config.get('fiat_display_currency', '') try: stats = self._rpc_trade_statistics( stake_cur, fiat_disp_cur) profit_closed_coin = stats['profit_closed_coin'] profit_closed_percent = stats['profit_closed_percent'] profit_closed_fiat = stats['profit_closed_fiat'] profit_all_coin = stats['profit_all_coin'] profit_all_percent = stats['profit_all_percent'] profit_all_fiat = stats['profit_all_fiat'] trade_count = stats['trade_count'] first_trade_date = stats['first_trade_date'] latest_trade_date = stats['latest_trade_date'] avg_duration = stats['avg_duration'] best_pair = stats['best_pair'] best_rate = stats['best_rate'] # Message to display markdown_msg = "*ROI:* Close trades\n" \ f"∙ `{profit_closed_coin:.8f} {stake_cur} "\ f"({profit_closed_percent:.2f}%)`\n" \ f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n" \ f"*ROI:* All trades\n" \ f"∙ `{profit_all_coin:.8f} {stake_cur} ({profit_all_percent:.2f}%)`\n" \ f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" \ f"*Total Trade Count:* `{trade_count}`\n" \ f"*First Trade opened:* `{first_trade_date}`\n" \ f"*Latest Trade opened:* `{latest_trade_date}`\n" \ f"*Avg. Duration:* `{avg_duration}`\n" \ f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`" self._send_msg(markdown_msg) except RPCException as e: self._send_msg(str(e)) @authorized_only def _balance(self, update: Update, context: CallbackContext) -> None: """ Handler for /balance """ try: result = self._rpc_balance(self._config.get('fiat_display_currency', '')) output = '' for currency in result['currencies']: if currency['est_btc'] > 0.0001: curr_output = "*{currency}:*\n" \ "\t`Available: {free: .8f}`\n" \ "\t`Balance: {balance: .8f}`\n" \ "\t`Pending: {used: .8f}`\n" \ "\t`Est. BTC: {est_btc: .8f}`\n".format(**currency) else: curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency) # Handle overflowing messsage length if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: self._send_msg(output) output = curr_output else: output += curr_output output += "\n*Estimated Value*:\n" \ "\t`BTC: {total: .8f}`\n" \ "\t`{symbol}: {value: .2f}`\n".format(**result) self._send_msg(output) except RPCException as e: self._send_msg(str(e)) @authorized_only def _start(self, update: Update, context: CallbackContext) -> None: """ Handler for /start. Starts TradeThread :param bot: telegram bot :param update: message update :return: None """ msg = self._rpc_start() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only def _stop(self, update: Update, context: CallbackContext) -> None: """ Handler for /stop. Stops TradeThread :param bot: telegram bot :param update: message update :return: None """ msg = self._rpc_stop() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only def _reload_conf(self, update: Update, context: CallbackContext) -> None: """ Handler for /reload_conf. Triggers a config file reload :param bot: telegram bot :param update: message update :return: None """ msg = self._rpc_reload_conf() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only def _stopbuy(self, update: Update, context: CallbackContext) -> None: """ Handler for /stop_buy. Sets max_open_trades to 0 and gracefully sells all open trades :param bot: telegram bot :param update: message update :return: None """ msg = self._rpc_stopbuy() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only def _forcesell(self, update: Update, context: CallbackContext) -> None: """ Handler for /forcesell <id>. Sells the given trade at current price :param bot: telegram bot :param update: message update :return: None """ trade_id = context.args[0] if len(context.args) > 0 else None try: msg = self._rpc_forcesell(trade_id) self._send_msg('Forcesell Result: `{result}`'.format(**msg)) except RPCException as e: self._send_msg(str(e)) @authorized_only def _forcebuy(self, update: Update, context: CallbackContext) -> None: """ Handler for /forcebuy <asset> <price>. Buys a pair trade at the given or current price :param bot: telegram bot :param update: message update :return: None """ pair = context.args[0] price = float(context.args[1]) if len(context.args) > 1 else None try: self._rpc_forcebuy(pair, price) except RPCException as e: self._send_msg(str(e)) @authorized_only def _performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /performance. Shows a performance statistic from finished trades :param bot: telegram bot :param update: message update :return: None """ try: trades = self._rpc_performance() stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format( index=i + 1, pair=trade['pair'], profit=trade['profit'], count=trade['count'] ) for i, trade in enumerate(trades)) message = '<b>Performance:</b>\n{}'.format(stats) self._send_msg(message, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) @authorized_only def _count(self, update: Update, context: CallbackContext) -> None: """ Handler for /count. Returns the number of trades running :param bot: telegram bot :param update: message update :return: None """ try: counts = self._rpc_count() message = tabulate({k: [v] for k, v in counts.items()}, headers=['current', 'max', 'total stake'], tablefmt='simple') message = "<pre>{}</pre>".format(message) logger.debug(message) self._send_msg(message, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) @authorized_only def _whitelist(self, update: Update, context: CallbackContext) -> None: """ Handler for /whitelist Shows the currently active whitelist """ try: whitelist = self._rpc_whitelist() message = f"Using whitelist `{whitelist['method']}` with {whitelist['length']} pairs\n" message += f"`{', '.join(whitelist['whitelist'])}`" logger.debug(message) self._send_msg(message) except RPCException as e: self._send_msg(str(e)) @authorized_only def _blacklist(self, update: Update, context: CallbackContext) -> None: """ Handler for /blacklist Shows the currently active blacklist """ try: blacklist = self._rpc_blacklist(context.args) message = f"Blacklist contains {blacklist['length']} pairs\n" message += f"`{', '.join(blacklist['blacklist'])}`" logger.debug(message) self._send_msg(message) except RPCException as e: self._send_msg(str(e)) @authorized_only def _edge(self, update: Update, context: CallbackContext) -> None: """ Handler for /edge Shows information related to Edge """ try: edge_pairs = self._rpc_edge() edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple') message = f'<b>Edge only validated following pairs:</b>\n<pre>{edge_pairs_tab}</pre>' self._send_msg(message, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) @authorized_only def _help(self, update: Update, context: CallbackContext) -> None: """ Handler for /help. Show commands of the bot :param bot: telegram bot :param update: message update :return: None """ forcebuy_text = "*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. " \ "Optionally takes a rate at which to buy.` \n" message = "*/start:* `Starts the trader`\n" \ "*/stop:* `Stops the trader`\n" \ "*/status [table]:* `Lists all open trades`\n" \ " *table :* `will display trades in a table`\n" \ "*/profit:* `Lists cumulative profit from all finished trades`\n" \ "*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, " \ "regardless of profit`\n" \ f"{forcebuy_text if self._config.get('forcebuy_enable', False) else '' }" \ "*/performance:* `Show performance of each finished trade grouped by pair`\n" \ "*/daily <n>:* `Shows profit or loss per day, over the last n days`\n" \ "*/count:* `Show number of trades running compared to allowed number of trades`" \ "\n" \ "*/balance:* `Show account balance per currency`\n" \ "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \ "*/reload_conf:* `Reload configuration file` \n" \ "*/whitelist:* `Show current whitelist` \n" \ "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " \ "to the blacklist.` \n" \ "*/edge:* `Shows validated pairs by Edge if it is enabeld` \n" \ "*/help:* `This help message`\n" \ "*/version:* `Show version`" self._send_msg(message) @authorized_only def _version(self, update: Update, context: CallbackContext) -> None: """ Handler for /version. Show version information :param bot: telegram bot :param update: message update :return: None """ self._send_msg('*Version:* `{}`'.format(__version__)) def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: """ Send given markdown message :param msg: message :param bot: alternative bot :param parse_mode: telegram parse mode :return: None """ keyboard = [['/daily', '/profit', '/balance'], ['/status', '/status table', '/performance'], ['/count', '/start', '/stop', '/help']] reply_markup = ReplyKeyboardMarkup(keyboard) try: try: self._updater.bot.send_message( self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, reply_markup=reply_markup ) except NetworkError as network_err: # Sometimes the telegram server resets the current connection, # if this is the case we send the message again. logger.warning( 'Telegram NetworkError: %s! Trying one more time.', network_err.message ) self._updater.bot.send_message( self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, reply_markup=reply_markup ) except TelegramError as telegram_err: logger.warning( 'TelegramError: %s! Giving up on that message.', telegram_err.message )
class Telegram(RPC): """ This class handles all telegram communication """ def __init__(self, freqtrade) -> None: """ Init the Telegram call, and init the super class RPC :param freqtrade: Instance of a freqtrade bot :return: None """ super().__init__(freqtrade) self._updater: Updater = None self._config = freqtrade.config self._init() if self._config.get('fiat_display_currency', None): self._fiat_converter = CryptoToFiatConverter() def _init(self) -> None: """ Initializes this module with the given config, registers all known command handlers and starts polling for message updates """ self._updater = Updater(token=self._config['telegram']['token'], workers=0, use_context=True) # Register command handler and start telegram message polling handles = [ CommandHandler('status', self._status), CommandHandler('profit', self._profit), CommandHandler('balance', self._balance), CommandHandler('start', self._start), CommandHandler('stop', self._stop), CommandHandler('forcesell', self._forcesell), CommandHandler('forcebuy', self._forcebuy), CommandHandler('trades', self._trades), CommandHandler('delete', self._delete_trade), CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), CommandHandler(['reload_config', 'reload_conf'], self._reload_config), CommandHandler(['show_config', 'show_conf'], self._show_config), CommandHandler('stopbuy', self._stopbuy), CommandHandler('whitelist', self._whitelist), CommandHandler('blacklist', self._blacklist), CommandHandler('logs', self._logs), CommandHandler('edge', self._edge), CommandHandler('help', self._help), CommandHandler('version', self._version), ] for handle in handles: self._updater.dispatcher.add_handler(handle) self._updater.start_polling( clean=True, bootstrap_retries=-1, timeout=30, read_latency=60, ) logger.info( 'rpc.telegram is listening for following commands: %s', [h.command for h in handles] ) def cleanup(self) -> None: """ Stops all running telegram threads. :return: None """ self._updater.stop() def send_msg(self, msg: Dict[str, Any]) -> None: """ Send a message to telegram channel """ noti = self._config['telegram'].get('notification_settings', {} ).get(str(msg['type']), 'on') if noti == 'off': logger.info(f"Notification '{msg['type']}' not sent.") # Notification disabled return if msg['type'] == RPCMessageType.BUY_NOTIFICATION: if self._fiat_converter: msg['stake_amount_fiat'] = self._fiat_converter.convert_amount( msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) else: msg['stake_amount_fiat'] = 0 message = ("\N{LARGE BLUE CIRCLE} *{exchange}:* Buying {pair}\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{limit:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" "*Total:* `({stake_amount:.6f} {stake_currency}").format(**msg) if msg.get('fiat_currency', None): message += ", {stake_amount_fiat:.3f} {fiat_currency}".format(**msg) message += ")`" elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: message = ("\N{WARNING SIGN} *{exchange}:* " "Cancelling open buy Order for {pair}. Reason: {reason}.".format(**msg)) elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: msg['amount'] = round(msg['amount'], 8) msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) msg['duration'] = msg['close_date'].replace( microsecond=0) - msg['open_date'].replace(microsecond=0) msg['duration_min'] = msg['duration'].total_seconds() / 60 msg['emoji'] = self._get_sell_emoji(msg) message = ("{emoji} *{exchange}:* Selling {pair}\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" "*Close Rate:* `{limit:.8f}`\n" "*Sell Reason:* `{sell_reason}`\n" "*Duration:* `{duration} ({duration_min:.1f} min)`\n" "*Profit:* `{profit_percent:.2f}%`").format(**msg) # Check if all sell properties are available. # This might not be the case if the message origin is triggered by /forcesell if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) and self._fiat_converter): msg['profit_fiat'] = self._fiat_converter.convert_amount( msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) message += (' `({gain}: {profit_amount:.8f} {stake_currency}' ' / {profit_fiat:.3f} {fiat_currency})`').format(**msg) elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order " "for {pair}. Reason: {reason}").format(**msg) elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: message = '*Status:* `{status}`'.format(**msg) elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION: message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg) elif msg['type'] == RPCMessageType.STARTUP_NOTIFICATION: message = '{status}'.format(**msg) else: raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) self._send_msg(message, disable_notification=(noti == 'silent')) def _get_sell_emoji(self, msg): """ Get emoji for sell-side """ if float(msg['profit_percent']) >= 5.0: return "\N{ROCKET}" elif float(msg['profit_percent']) >= 0.0: return "\N{EIGHT SPOKED ASTERISK}" elif msg['sell_reason'] == "stop_loss": return"\N{WARNING SIGN}" else: return "\N{CROSS MARK}" @authorized_only def _status(self, update: Update, context: CallbackContext) -> None: """ Handler for /status. Returns the current TradeThread status :param bot: telegram bot :param update: message update :return: None """ if 'table' in context.args: self._status_table(update, context) return try: results = self._rpc_trade_status() messages = [] for r in results: lines = [ "*Trade ID:* `{trade_id}` `(since {open_date_hum})`", "*Current Pair:* {pair}", "*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Open Rate:* `{open_rate:.8f}`", "*Close Rate:* `{close_rate}`" if r['close_rate'] else "", "*Current Rate:* `{current_rate:.8f}`", ("*Close Profit:* `{close_profit_pct}`" if r['close_profit_pct'] is not None else ""), "*Current Profit:* `{current_profit_pct:.2f}%`", ] if (r['stop_loss'] != r['initial_stop_loss'] and r['initial_stop_loss_pct'] is not None): # Adding initial stoploss only if it is different from stoploss lines.append("*Initial Stoploss:* `{initial_stop_loss:.8f}` " "`({initial_stop_loss_pct:.2f}%)`") # Adding stoploss and stoploss percentage only if it is not None lines.append("*Stoploss:* `{stop_loss:.8f}` " + ("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else "")) lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` " "`({stoploss_current_dist_pct:.2f}%)`") if r['open_order']: if r['sell_order_status']: lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`") else: lines.append("*Open Order:* `{open_order}`") # Filter empty lines using list-comprehension messages.append("\n".join([line for line in lines if line]).format(**r)) for msg in messages: self._send_msg(msg) except RPCException as e: self._send_msg(str(e)) @authorized_only def _status_table(self, update: Update, context: CallbackContext) -> None: """ Handler for /status table. Returns the current TradeThread status in table format :param bot: telegram bot :param update: message update :return: None """ try: statlist, head = self._rpc_status_table(self._config['stake_currency'], self._config.get('fiat_display_currency', '')) message = tabulate(statlist, headers=head, tablefmt='simple') self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) @authorized_only def _daily(self, update: Update, context: CallbackContext) -> None: """ Handler for /daily <n> Returns a daily profit (in BTC) over the last n days. :param bot: telegram bot :param update: message update :return: None """ stake_cur = self._config['stake_currency'] fiat_disp_cur = self._config.get('fiat_display_currency', '') try: timescale = int(context.args[0]) except (TypeError, ValueError, IndexError): timescale = 7 try: stats = self._rpc_daily_profit( timescale, stake_cur, fiat_disp_cur ) stats_tab = tabulate( [[day['date'], f"{day['abs_profit']:.8f} {stats['stake_currency']}", f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}", f"{day['trade_count']} trades"] for day in stats['data']], headers=[ 'Day', f'Profit {stake_cur}', f'Profit {fiat_disp_cur}', 'Trades', ], tablefmt='simple') message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>' self._send_msg(message, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) @authorized_only def _profit(self, update: Update, context: CallbackContext) -> None: """ Handler for /profit. Returns a cumulative profit statistics. :param bot: telegram bot :param update: message update :return: None """ stake_cur = self._config['stake_currency'] fiat_disp_cur = self._config.get('fiat_display_currency', '') stats = self._rpc_trade_statistics( stake_cur, fiat_disp_cur) profit_closed_coin = stats['profit_closed_coin'] profit_closed_percent_mean = stats['profit_closed_percent_mean'] profit_closed_percent_sum = stats['profit_closed_percent_sum'] profit_closed_fiat = stats['profit_closed_fiat'] profit_all_coin = stats['profit_all_coin'] profit_all_percent_mean = stats['profit_all_percent_mean'] profit_all_percent_sum = stats['profit_all_percent_sum'] profit_all_fiat = stats['profit_all_fiat'] trade_count = stats['trade_count'] first_trade_date = stats['first_trade_date'] latest_trade_date = stats['latest_trade_date'] avg_duration = stats['avg_duration'] best_pair = stats['best_pair'] best_rate = stats['best_rate'] if stats['trade_count'] == 0: markdown_msg = 'No trades yet.' else: # Message to display if stats['closed_trade_count'] > 0: markdown_msg = ("*ROI:* Closed trades\n" f"∙ `{profit_closed_coin:.8f} {stake_cur} " f"({profit_closed_percent_mean:.2f}%) " f"({profit_closed_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n") else: markdown_msg = "`No closed trade` \n" markdown_msg += (f"*ROI:* All trades\n" f"∙ `{profit_all_coin:.8f} {stake_cur} " f"({profit_all_percent_mean:.2f}%) " f"({profit_all_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" f"*Total Trade Count:* `{trade_count}`\n" f"*First Trade opened:* `{first_trade_date}`\n" f"*Latest Trade opened:* `{latest_trade_date}\n`" f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`" ) if stats['closed_trade_count'] > 0: markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n" f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`") self._send_msg(markdown_msg) @authorized_only def _balance(self, update: Update, context: CallbackContext) -> None: """ Handler for /balance """ try: result = self._rpc_balance(self._config['stake_currency'], self._config.get('fiat_display_currency', '')) output = '' if self._config['dry_run']: output += ( f"*Warning:* Simulated balances in Dry Mode.\n" "This mode is still experimental!\n" "Starting capital: " f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" ) for currency in result['currencies']: if currency['est_stake'] > 0.0001: curr_output = ("*{currency}:*\n" "\t`Available: {free: .8f}`\n" "\t`Balance: {balance: .8f}`\n" "\t`Pending: {used: .8f}`\n" "\t`Est. {stake}: {est_stake: .8f}`\n").format(**currency) else: curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency) # Handle overflowing messsage length if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: self._send_msg(output) output = curr_output else: output += curr_output output += ("\n*Estimated Value*:\n" "\t`{stake}: {total: .8f}`\n" "\t`{symbol}: {value: .2f}`\n").format(**result) self._send_msg(output) except RPCException as e: self._send_msg(str(e)) @authorized_only def _start(self, update: Update, context: CallbackContext) -> None: """ Handler for /start. Starts TradeThread :param bot: telegram bot :param update: message update :return: None """ msg = self._rpc_start() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only def _stop(self, update: Update, context: CallbackContext) -> None: """ Handler for /stop. Stops TradeThread :param bot: telegram bot :param update: message update :return: None """ msg = self._rpc_stop() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only def _reload_config(self, update: Update, context: CallbackContext) -> None: """ Handler for /reload_config. Triggers a config file reload :param bot: telegram bot :param update: message update :return: None """ msg = self._rpc_reload_config() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only def _stopbuy(self, update: Update, context: CallbackContext) -> None: """ Handler for /stop_buy. Sets max_open_trades to 0 and gracefully sells all open trades :param bot: telegram bot :param update: message update :return: None """ msg = self._rpc_stopbuy() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only def _forcesell(self, update: Update, context: CallbackContext) -> None: """ Handler for /forcesell <id>. Sells the given trade at current price :param bot: telegram bot :param update: message update :return: None """ trade_id = context.args[0] if len(context.args) > 0 else None try: msg = self._rpc_forcesell(trade_id) self._send_msg('Forcesell Result: `{result}`'.format(**msg)) except RPCException as e: self._send_msg(str(e)) @authorized_only def _forcebuy(self, update: Update, context: CallbackContext) -> None: """ Handler for /forcebuy <asset> <price>. Buys a pair trade at the given or current price :param bot: telegram bot :param update: message update :return: None """ pair = context.args[0] price = float(context.args[1]) if len(context.args) > 1 else None try: self._rpc_forcebuy(pair, price) except RPCException as e: self._send_msg(str(e)) @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: """ Handler for /trades <n> Returns last n recent trades. :param bot: telegram bot :param update: message update :return: None """ stake_cur = self._config['stake_currency'] try: nrecent = int(context.args[0]) except (TypeError, ValueError, IndexError): nrecent = 10 try: trades = self._rpc_trade_history( nrecent ) trades_tab = tabulate( [[arrow.get(trade['open_date']).humanize(), trade['pair'], f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"] for trade in trades['trades']], headers=[ 'Open Date', 'Pair', f'Profit ({stake_cur})', ], tablefmt='simple') message = (f"<b>{min(trades['trades_count'], nrecent)} recent trades</b>:\n" + (f"<pre>{trades_tab}</pre>" if trades['trades_count'] > 0 else '')) self._send_msg(message, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) @authorized_only def _delete_trade(self, update: Update, context: CallbackContext) -> None: """ Handler for /delete <id>. Delete the given trade :param bot: telegram bot :param update: message update :return: None """ trade_id = context.args[0] if len(context.args) > 0 else None try: msg = self._rpc_delete(trade_id) self._send_msg(( '`{result_msg}`\n' 'Please make sure to take care of this asset on the exchange manually.' ).format(**msg)) except RPCException as e: self._send_msg(str(e)) @authorized_only def _performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /performance. Shows a performance statistic from finished trades :param bot: telegram bot :param update: message update :return: None """ try: trades = self._rpc_performance() stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format( index=i + 1, pair=trade['pair'], profit=trade['profit'], count=trade['count'] ) for i, trade in enumerate(trades)) message = '<b>Performance:</b>\n{}'.format(stats) self._send_msg(message, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) @authorized_only def _count(self, update: Update, context: CallbackContext) -> None: """ Handler for /count. Returns the number of trades running :param bot: telegram bot :param update: message update :return: None """ try: counts = self._rpc_count() message = tabulate({k: [v] for k, v in counts.items()}, headers=['current', 'max', 'total stake'], tablefmt='simple') message = "<pre>{}</pre>".format(message) logger.debug(message) self._send_msg(message, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) @authorized_only def _whitelist(self, update: Update, context: CallbackContext) -> None: """ Handler for /whitelist Shows the currently active whitelist """ try: whitelist = self._rpc_whitelist() message = f"Using whitelist `{whitelist['method']}` with {whitelist['length']} pairs\n" message += f"`{', '.join(whitelist['whitelist'])}`" logger.debug(message) self._send_msg(message) except RPCException as e: self._send_msg(str(e)) @authorized_only def _blacklist(self, update: Update, context: CallbackContext) -> None: """ Handler for /blacklist Shows the currently active blacklist """ try: blacklist = self._rpc_blacklist(context.args) errmsgs = [] for pair, error in blacklist['errors'].items(): errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`") if errmsgs: self._send_msg('\n'.join(errmsgs)) message = f"Blacklist contains {blacklist['length']} pairs\n" message += f"`{', '.join(blacklist['blacklist'])}`" logger.debug(message) self._send_msg(message) except RPCException as e: self._send_msg(str(e)) @authorized_only def _logs(self, update: Update, context: CallbackContext) -> None: """ Handler for /logs Shows the latest logs """ try: try: limit = int(context.args[0]) except (TypeError, ValueError, IndexError): limit = 10 logs = self._rpc_get_logs(limit)['logs'] msgs = '' msg_template = "*{}* {}: {} \\- `{}`" for logrec in logs: msg = msg_template.format(escape_markdown(logrec[0], version=2), escape_markdown(logrec[2], version=2), escape_markdown(logrec[3], version=2), escape_markdown(logrec[4], version=2)) if len(msgs + msg) + 10 >= MAX_TELEGRAM_MESSAGE_LENGTH: # Send message immediately if it would become too long self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) msgs = msg + '\n' else: # Append message to messages to send msgs += msg + '\n' if msgs: self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) except RPCException as e: self._send_msg(str(e)) @authorized_only def _edge(self, update: Update, context: CallbackContext) -> None: """ Handler for /edge Shows information related to Edge """ try: edge_pairs = self._rpc_edge() edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple') message = f'<b>Edge only validated following pairs:</b>\n<pre>{edge_pairs_tab}</pre>' self._send_msg(message, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) @authorized_only def _help(self, update: Update, context: CallbackContext) -> None: """ Handler for /help. Show commands of the bot :param bot: telegram bot :param update: message update :return: None """ forcebuy_text = ("*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. " "Optionally takes a rate at which to buy.` \n") message = ("*/start:* `Starts the trader`\n" "*/stop:* `Stops the trader`\n" "*/status [table]:* `Lists all open trades`\n" " *table :* `will display trades in a table`\n" " `pending buy orders are marked with an asterisk (*)`\n" " `pending sell orders are marked with a double asterisk (**)`\n" "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n" "*/profit:* `Lists cumulative profit from all finished trades`\n" "*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, " "regardless of profit`\n" f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}" "*/delete <trade_id>:* `Instantly delete the given trade in the database`\n" "*/performance:* `Show performance of each finished trade grouped by pair`\n" "*/daily <n>:* `Shows profit or loss per day, over the last n days`\n" "*/count:* `Show number of trades running compared to allowed number of trades`" "\n" "*/balance:* `Show account balance per currency`\n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" "*/reload_config:* `Reload configuration file` \n" "*/show_config:* `Show running configuration` \n" "*/logs [limit]:* `Show latest logs - defaults to 10` \n" "*/whitelist:* `Show current whitelist` \n" "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " "to the blacklist.` \n" "*/edge:* `Shows validated pairs by Edge if it is enabled` \n" "*/help:* `This help message`\n" "*/version:* `Show version`") self._send_msg(message) @authorized_only def _version(self, update: Update, context: CallbackContext) -> None: """ Handler for /version. Show version information :param bot: telegram bot :param update: message update :return: None """ self._send_msg('*Version:* `{}`'.format(__version__)) @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: """ Handler for /show_config. Show config information information :param bot: telegram bot :param update: message update :return: None """ val = self._rpc_show_config() if val['trailing_stop']: sl_info = ( f"*Initial Stoploss:* `{val['stoploss']}`\n" f"*Trailing stop positive:* `{val['trailing_stop_positive']}`\n" f"*Trailing stop offset:* `{val['trailing_stop_positive_offset']}`\n" f"*Only trail above offset:* `{val['trailing_only_offset_is_reached']}`\n" ) else: sl_info = f"*Stoploss:* `{val['stoploss']}`\n" self._send_msg( f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n" f"*Exchange:* `{val['exchange']}`\n" f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n" f"*Max open Trades:* `{val['max_open_trades']}`\n" f"*Minimum ROI:* `{val['minimal_roi']}`\n" f"*Ask strategy:* ```\n{json.dumps(val['ask_strategy'])}```\n" f"*Bid strategy:* ```\n{json.dumps(val['bid_strategy'])}```\n" f"{sl_info}" f"*Timeframe:* `{val['timeframe']}`\n" f"*Strategy:* `{val['strategy']}`\n" f"*Current state:* `{val['state']}`" ) def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN, disable_notification: bool = False) -> None: """ Send given markdown message :param msg: message :param bot: alternative bot :param parse_mode: telegram parse mode :return: None """ keyboard = [['/daily', '/profit', '/balance'], ['/status', '/status table', '/performance'], ['/count', '/start', '/stop', '/help']] reply_markup = ReplyKeyboardMarkup(keyboard) try: try: self._updater.bot.send_message( self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, reply_markup=reply_markup, disable_notification=disable_notification, ) except NetworkError as network_err: # Sometimes the telegram server resets the current connection, # if this is the case we send the message again. logger.warning( 'Telegram NetworkError: %s! Trying one more time.', network_err.message ) self._updater.bot.send_message( self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, reply_markup=reply_markup, disable_notification=disable_notification, ) except TelegramError as telegram_err: logger.warning( 'TelegramError: %s! Giving up on that message.', telegram_err.message )