Merge branch 'develop' into pr/gmatheu/4746

This commit is contained in:
Matthias
2021-05-23 16:03:11 +02:00
166 changed files with 6354 additions and 2939 deletions

View File

@@ -57,6 +57,7 @@ class Count(BaseModel):
class PerformanceEntry(BaseModel):
pair: str
profit: float
profit_abs: float
count: int
@@ -151,13 +152,11 @@ class TradeSchema(BaseModel):
fee_close: Optional[float]
fee_close_cost: Optional[float]
fee_close_currency: Optional[str]
open_date_hum: str
open_date: str
open_timestamp: int
open_rate: float
open_rate_requested: Optional[float]
open_trade_value: float
close_date_hum: Optional[str]
close_date: Optional[str]
close_timestamp: Optional[int]
close_rate: Optional[float]
@@ -168,6 +167,7 @@ class TradeSchema(BaseModel):
profit_ratio: Optional[float]
profit_pct: Optional[float]
profit_abs: Optional[float]
profit_fiat: Optional[float]
sell_reason: Optional[str]
sell_order_status: Optional[str]
stop_loss_abs: Optional[float]
@@ -190,7 +190,6 @@ class OpenTradeSchema(TradeSchema):
stoploss_current_dist_ratio: Optional[float]
stoploss_entry_dist: Optional[float]
stoploss_entry_dist_ratio: Optional[float]
base_currency: str
current_profit: float
current_profit_abs: float
current_profit_pct: float
@@ -201,6 +200,7 @@ class OpenTradeSchema(TradeSchema):
class TradeResponse(BaseModel):
trades: List[TradeSchema]
trades_count: int
total_trades: int
class ForceBuyResponse(BaseModel):
@@ -269,7 +269,7 @@ class DeleteTrade(BaseModel):
class PlotConfig_(BaseModel):
main_plot: Dict[str, Any]
subplots: Optional[Dict[str, Any]]
subplots: Dict[str, Any]
class PlotConfig(BaseModel):

View File

@@ -17,8 +17,7 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac
OpenTradeSchema, PairHistory, PerformanceEntry,
Ping, PlotConfig, Profit, ResultMsg, ShowConfig,
Stats, StatusMsg, StrategyListResponse,
StrategyResponse, TradeResponse, Version,
WhitelistResponse)
StrategyResponse, Version, WhitelistResponse)
from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional
from freqtrade.rpc.rpc import RPCException
@@ -83,9 +82,19 @@ def status(rpc: RPC = Depends(get_rpc)):
return []
@router.get('/trades', response_model=TradeResponse, tags=['info', 'trading'])
def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_trade_history(limit)
# Using the responsemodel here will cause a ~100% increase in response time (from 1s to 2s)
# on big databases. Correct response model: response_model=TradeResponse,
@router.get('/trades', tags=['info', 'trading'])
def trades(limit: int = 500, offset: int = 0, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_trade_history(limit, offset=offset, order_by_id=True)
@router.get('/trade/{tradeid}', response_model=OpenTradeSchema, tags=['info', 'trading'])
def trade(tradeid: int = 0, rpc: RPC = Depends(get_rpc)):
try:
return rpc._rpc_trade_status([tradeid])[0]
except (RPCException, KeyError):
raise HTTPException(status_code=404, detail='Trade not found.')
@router.delete('/trades/{tradeid}', response_model=DeleteTrade, tags=['info', 'trading'])

View File

@@ -13,6 +13,11 @@ async def favicon():
return FileResponse(str(Path(__file__).parent / 'ui/favicon.ico'))
@router_ui.get('/fallback_file.html', include_in_schema=False)
async def fallback():
return FileResponse(str(Path(__file__).parent / 'ui/fallback_file.html'))
@router_ui.get('/{rest_of_path:path}', include_in_schema=False)
async def index_html(rest_of_path: str):
"""

View File

@@ -3,11 +3,13 @@ Module that define classes to convert Crypto-currency to FIAT
e.g BTC to USD
"""
import datetime
import logging
import time
from typing import Dict, List
from typing import Dict
from cachetools.ttl import TTLCache
from pycoingecko import CoinGeckoAPI
from requests.exceptions import RequestException
from freqtrade.constants import SUPPORTED_FIAT
@@ -15,51 +17,6 @@ from freqtrade.constants import SUPPORTED_FIAT
logger = logging.getLogger(__name__)
class CryptoFiat:
"""
Object to describe what is the price of Crypto-currency in a FIAT
"""
# Constants
CACHE_DURATION = 6 * 60 * 60 # 6 hours
def __init__(self, crypto_symbol: str, fiat_symbol: str, price: float) -> None:
"""
Create an object that will contains the price for a crypto-currency in fiat
:param crypto_symbol: Crypto-currency you want to convert (e.g BTC)
:param fiat_symbol: FIAT currency you want to convert to (e.g USD)
:param price: Price in FIAT
"""
# Public attributes
self.crypto_symbol = None
self.fiat_symbol = None
self.price = 0.0
# Private attributes
self._expiration = 0.0
self.crypto_symbol = crypto_symbol.lower()
self.fiat_symbol = fiat_symbol.lower()
self.set_price(price=price)
def set_price(self, price: float) -> None:
"""
Set the price of the Crypto-currency in FIAT and set the expiration time
:param price: Price of the current Crypto currency in the fiat
:return: None
"""
self.price = price
self._expiration = time.time() + self.CACHE_DURATION
def is_expired(self) -> bool:
"""
Return if the current price is still valid or needs to be refreshed
:return: bool, true the price is expired and needs to be refreshed, false the price is
still valid
"""
return self._expiration - time.time() <= 0
class CryptoToFiatConverter:
"""
Main class to initiate Crypto to FIAT.
@@ -70,6 +27,7 @@ class CryptoToFiatConverter:
_coingekko: CoinGeckoAPI = None
_cryptomap: Dict = {}
_backoff: float = 0.0
def __new__(cls):
"""
@@ -84,14 +42,29 @@ class CryptoToFiatConverter:
return CryptoToFiatConverter.__instance
def __init__(self) -> None:
self._pairs: List[CryptoFiat] = []
# Timeout: 6h
self._pair_price: TTLCache = TTLCache(maxsize=500, ttl=6 * 60 * 60)
self._load_cryptomap()
def _load_cryptomap(self) -> None:
try:
coinlistings = self._coingekko.get_coins_list()
# Create mapping table from synbol to coingekko_id
# Create mapping table from symbol to coingekko_id
self._cryptomap = {x['symbol']: x['id'] for x in coinlistings}
except RequestException as request_exception:
if "429" in str(request_exception):
logger.warning(
"Too many requests for Coingecko API, backing off and trying again later.")
# Set backoff timestamp to 60 seconds in the future
self._backoff = datetime.datetime.now().timestamp() + 60
return
# If the request is not a 429 error we want to raise the normal error
logger.error(
"Could not load FIAT Cryptocurrency map for the following problem: {}".format(
request_exception
)
)
except (Exception) as exception:
logger.error(
f"Could not load FIAT Cryptocurrency map for the following problem: {exception}")
@@ -118,49 +91,31 @@ class CryptoToFiatConverter:
"""
crypto_symbol = crypto_symbol.lower()
fiat_symbol = fiat_symbol.lower()
inverse = False
if crypto_symbol == 'usd':
# usd corresponds to "uniswap-state-dollar" for coingecko.
# We'll therefore need to "swap" the currencies
logger.info(f"reversing Rates {crypto_symbol}, {fiat_symbol}")
crypto_symbol = fiat_symbol
fiat_symbol = 'usd'
inverse = True
symbol = f"{crypto_symbol}/{fiat_symbol}"
# Check if the fiat convertion you want is supported
if not self._is_supported_fiat(fiat=fiat_symbol):
raise ValueError(f'The fiat {fiat_symbol} is not supported.')
# Get the pair that interest us and return the price in fiat
for pair in self._pairs:
if pair.crypto_symbol == crypto_symbol and pair.fiat_symbol == fiat_symbol:
# If the price is expired we refresh it, avoid to call the API all the time
if pair.is_expired():
pair.set_price(
price=self._find_price(
crypto_symbol=pair.crypto_symbol,
fiat_symbol=pair.fiat_symbol
)
)
price = self._pair_price.get(symbol, None)
# return the last price we have for this pair
return pair.price
# The pair does not exist, so we create it and return the price
return self._add_pair(
crypto_symbol=crypto_symbol,
fiat_symbol=fiat_symbol,
price=self._find_price(
if not price:
price = self._find_price(
crypto_symbol=crypto_symbol,
fiat_symbol=fiat_symbol
)
)
def _add_pair(self, crypto_symbol: str, fiat_symbol: str, price: float) -> float:
"""
:param crypto_symbol: Crypto-currency you want to convert (e.g BTC)
:param fiat_symbol: FIAT currency you want to convert to (e.g USD)
:return: price in FIAT
"""
self._pairs.append(
CryptoFiat(
crypto_symbol=crypto_symbol,
fiat_symbol=fiat_symbol,
price=price
)
)
if inverse and price != 0.0:
price = 1 / price
self._pair_price[symbol] = price
return price
@@ -188,6 +143,15 @@ class CryptoToFiatConverter:
if crypto_symbol == fiat_symbol:
return 1.0
if self._cryptomap == {}:
if self._backoff <= datetime.datetime.now().timestamp():
self._load_cryptomap()
# return 0.0 if we still dont have data to check, no reason to proceed
if self._cryptomap == {}:
return 0.0
else:
return 0.0
if crypto_symbol not in self._cryptomap:
# return 0 for unsupported stake currencies (fiat-convert should not break the bot)
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)

View File

@@ -24,20 +24,22 @@ from freqtrade.persistence.models import PairLock
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.state import State
from freqtrade.strategy.interface import SellType
from freqtrade.strategy.interface import SellCheckTuple, SellType
logger = logging.getLogger(__name__)
class RPCMessageType(Enum):
STATUS_NOTIFICATION = 'status'
WARNING_NOTIFICATION = 'warning'
STARTUP_NOTIFICATION = 'startup'
BUY_NOTIFICATION = 'buy'
BUY_CANCEL_NOTIFICATION = 'buy_cancel'
SELL_NOTIFICATION = 'sell'
SELL_CANCEL_NOTIFICATION = 'sell_cancel'
STATUS = 'status'
WARNING = 'warning'
STARTUP = 'startup'
BUY = 'buy'
BUY_FILL = 'buy_fill'
BUY_CANCEL = 'buy_cancel'
SELL = 'sell'
SELL_FILL = 'sell_fill'
SELL_CANCEL = 'sell_cancel'
def __repr__(self):
return self.value
@@ -167,12 +169,24 @@ class RPC:
if trade.open_order_id:
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
# calculate profit and send message to user
try:
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
except (ExchangeError, PricingError):
current_rate = NAN
if trade.is_open:
try:
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
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)
@@ -191,6 +205,7 @@ class RPC:
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),
@@ -205,12 +220,13 @@ class RPC:
return results
def _rpc_status_table(self, stake_currency: str,
fiat_display_currency: str) -> Tuple[List, List]:
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:
@@ -228,6 +244,8 @@ class RPC:
)
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
@@ -241,7 +259,7 @@ class RPC:
profitcol += " (" + fiat_display_currency + ")"
columns = ['ID', 'Pair', 'Since', profitcol]
return trades_list, columns
return trades_list, columns, fiat_profit_sum
def _rpc_daily_profit(
self, timescale: int,
@@ -285,11 +303,12 @@ class RPC:
'data': data
}
def _rpc_trade_history(self, limit: int) -> Dict:
def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict:
""" Returns the X last trades """
if limit > 0:
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(
Trade.close_date.desc()).limit(limit)
order_by).limit(limit).offset(offset)
else:
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
Trade.close_date.desc()).all()
@@ -298,7 +317,8 @@ class RPC:
return {
"trades": output,
"trades_count": len(output)
"trades_count": len(output),
"total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(),
}
def _rpc_stats(self) -> Dict[str, Any]:
@@ -432,7 +452,7 @@ class RPC:
output = []
total = 0.0
try:
tickers = self._freqtrade.exchange.get_tickers()
tickers = self._freqtrade.exchange.get_tickers(cached=True)
except (ExchangeError):
raise RPCException('Error getting current tickers.')
@@ -537,7 +557,8 @@ class RPC:
if not fully_canceled:
# Get current rate and execute sell
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL)
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
self._freqtrade.execute_sell(trade, current_rate, sell_reason)
# ---- EOF def _exec_forcesell ----
if self._freqtrade.state != State.RUNNING:
@@ -548,7 +569,7 @@ class RPC:
# Execute sell for all open orders
for trade in Trade.get_open_trades():
_exec_forcesell(trade)
Trade.session.flush()
Trade.query.session.flush()
self._freqtrade.wallets.update()
return {'result': 'Created sell orders for all open trades.'}
@@ -561,7 +582,7 @@ class RPC:
raise RPCException('invalid argument')
_exec_forcesell(trade)
Trade.session.flush()
Trade.query.session.flush()
self._freqtrade.wallets.update()
return {'result': f'Created sell order for trade {trade_id}.'}
@@ -590,8 +611,7 @@ class RPC:
raise RPCException(f'position for {pair} already open - id: {trade.id}')
# gen stake amount
stakeamount = self._freqtrade.wallets.get_trade_stake_amount(
pair, self._freqtrade.get_free_open_trades())
stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair)
# execute buy
if self._freqtrade.execute_buy(pair, stakeamount, price, forcebuy=True):
@@ -686,7 +706,7 @@ class RPC:
lock.lock_end_time = datetime.now(timezone.utc)
# session is always the same
PairLock.session.flush()
PairLock.query.session.flush()
return self._rpc_locks()
@@ -828,5 +848,7 @@ class RPC:
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

View File

@@ -67,7 +67,7 @@ class RPCManager:
def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None:
if config['dry_run']:
self.send_msg({
'type': RPCMessageType.WARNING_NOTIFICATION,
'type': RPCMessageType.WARNING,
'status': 'Dry run is enabled. All trades are simulated.'
})
stake_currency = config['stake_currency']
@@ -79,7 +79,7 @@ class RPCManager:
exchange_name = config['exchange']['name']
strategy_name = config.get('strategy', '')
self.send_msg({
'type': RPCMessageType.STARTUP_NOTIFICATION,
'type': RPCMessageType.STARTUP,
'status': f'*Exchange:* `{exchange_name}`\n'
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
f'*Minimum ROI:* `{minimal_roi}`\n'
@@ -88,13 +88,13 @@ class RPCManager:
f'*Strategy:* `{strategy_name}`'
})
self.send_msg({
'type': RPCMessageType.STARTUP_NOTIFICATION,
'type': RPCMessageType.STARTUP,
'status': f'Searching for {stake_currency} pairs to buy and sell '
f'based on {pairlist.short_desc()}'
})
if len(protections.name_list) > 0:
prots = '\n'.join([p for prot in protections.short_desc() for k, p in prot.items()])
self.send_msg({
'type': RPCMessageType.STARTUP_NOTIFICATION,
'type': RPCMessageType.STARTUP,
'status': f'Using Protections: \n{prots}'
})

View File

@@ -8,6 +8,7 @@ import logging
from datetime import timedelta
from html import escape
from itertools import chain
from math import isnan
from typing import Any, Callable, Dict, List, Optional, Union, cast
import arrow
@@ -21,7 +22,7 @@ from telegram.utils.helpers import escape_markdown
from freqtrade.__init__ import __version__
from freqtrade.constants import DUST_PER_COIN
from freqtrade.exceptions import OperationalException
from freqtrade.misc import round_coin_value
from freqtrade.misc import chunks, round_coin_value
from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType
@@ -160,10 +161,10 @@ class Telegram(RPCHandler):
for handle in handles:
self._updater.dispatcher.add_handler(handle)
self._updater.start_polling(
clean=True,
bootstrap_retries=-1,
timeout=30,
read_latency=60,
drop_pending_updates=True,
)
logger.info(
'rpc.telegram is listening for following commands: %s',
@@ -182,6 +183,53 @@ class Telegram(RPCHandler):
"""
self._updater.stop()
def _format_buy_msg(self, msg: Dict[str, Any]) -> str:
if self._rpc._fiat_converter:
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
else:
msg['stake_amount_fiat'] = 0
message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}"
f" (#{msg['trade_id']})\n"
f"*Amount:* `{msg['amount']:.8f}`\n"
f"*Open Rate:* `{msg['limit']:.8f}`\n"
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}")
if msg.get('fiat_currency', None):
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
message += ")`"
return message
def _format_sell_msg(self, msg: Dict[str, Any]) -> str:
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} (#{trade_id})\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._rpc._fiat_converter):
msg['profit_fiat'] = self._rpc._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)
return message
def send_msg(self, msg: Dict[str, Any]) -> None:
""" Send a message to telegram channel """
@@ -192,67 +240,33 @@ class Telegram(RPCHandler):
# Notification disabled
return
if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
if self._rpc._fiat_converter:
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
else:
msg['stake_amount_fiat'] = 0
if msg['type'] == RPCMessageType.BUY:
message = self._format_buy_msg(msg)
message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}"
f" (#{msg['trade_id']})\n"
f"*Amount:* `{msg['amount']:.8f}`\n"
f"*Open Rate:* `{msg['limit']:.8f}`\n"
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}")
if msg.get('fiat_currency', None):
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
message += ")`"
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
elif msg['type'] in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
msg['message_side'] = 'buy' if msg['type'] == RPCMessageType.BUY_CANCEL else 'sell'
message = ("\N{WARNING SIGN} *{exchange}:* "
"Cancelling open buy Order for {pair} (#{trade_id}). "
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
"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
elif msg['type'] == RPCMessageType.BUY_FILL:
message = ("\N{LARGE CIRCLE} *{exchange}:* "
"Buy order for {pair} (#{trade_id}) filled "
"for {open_rate}.".format(**msg))
elif msg['type'] == RPCMessageType.SELL_FILL:
message = ("\N{LARGE CIRCLE} *{exchange}:* "
"Sell order for {pair} (#{trade_id}) filled "
"for {close_rate}.".format(**msg))
elif msg['type'] == RPCMessageType.SELL:
message = self._format_sell_msg(msg)
msg['emoji'] = self._get_sell_emoji(msg)
message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\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._rpc._fiat_converter):
msg['profit_fiat'] = self._rpc._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} (#{trade_id}). Reason: {reason}").format(**msg)
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
elif msg['type'] == RPCMessageType.STATUS:
message = '*Status:* `{status}`'.format(**msg)
elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION:
elif msg['type'] == RPCMessageType.WARNING:
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
elif msg['type'] == RPCMessageType.STARTUP_NOTIFICATION:
elif msg['type'] == RPCMessageType.STARTUP:
message = '{status}'.format(**msg)
else:
@@ -300,6 +314,7 @@ class Telegram(RPCHandler):
messages = []
for r in results:
r['open_date_hum'] = arrow.get(r['open_date']).humanize()
lines = [
"*Trade ID:* `{trade_id}` `(since {open_date_hum})`",
"*Current Pair:* {pair}",
@@ -346,19 +361,31 @@ class Telegram(RPCHandler):
:return: None
"""
try:
statlist, head = self._rpc._rpc_status_table(
self._config['stake_currency'], self._config.get('fiat_display_currency', ''))
fiat_currency = self._config.get('fiat_display_currency', '')
statlist, head, fiat_profit_sum = self._rpc._rpc_status_table(
self._config['stake_currency'], fiat_currency)
show_total = not isnan(fiat_profit_sum) and len(statlist) > 1
max_trades_per_msg = 50
"""
Calculate the number of messages of 50 trades per message
0.99 is used to make sure that there are no extra (empty) messages
As an example with 50 trades, there will be int(50/50 + 0.99) = 1 message
"""
for i in range(0, max(int(len(statlist) / max_trades_per_msg + 0.99), 1)):
message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg],
messages_count = max(int(len(statlist) / max_trades_per_msg + 0.99), 1)
for i in range(0, messages_count):
trades = statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg]
if show_total and i == messages_count - 1:
# append total line
trades.append(["Total", "", "", f"{fiat_profit_sum:.2f} {fiat_currency}"])
message = tabulate(trades,
headers=head,
tablefmt='simple')
if show_total and i == messages_count - 1:
# insert separators line between Total
lines = message.split("\n")
message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]])
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e))
@@ -723,14 +750,21 @@ class Telegram(RPCHandler):
"""
try:
trades = self._rpc._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)
output = "<b>Performance:</b>\n"
for i, trade in enumerate(trades):
stat_line = (
f"{i+1}.\t <code>{trade['pair']}\t"
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
f"({trade['profit']:.2f}%) "
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line
else:
output += stat_line
self._send_msg(output, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e))
@@ -760,17 +794,21 @@ class Telegram(RPCHandler):
Handler for /locks.
Returns the currently active locks
"""
locks = self._rpc._rpc_locks()
message = tabulate([[
lock['id'],
lock['pair'],
lock['lock_end_time'],
lock['reason']] for lock in locks['locks']],
headers=['ID', 'Pair', 'Until', 'Reason'],
tablefmt='simple')
message = f"<pre>{escape(message)}</pre>"
logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML)
rpc_locks = self._rpc._rpc_locks()
if not rpc_locks['locks']:
self._send_msg('No active locks.', parse_mode=ParseMode.HTML)
for locks in chunks(rpc_locks['locks'], 25):
message = tabulate([[
lock['id'],
lock['pair'],
lock['lock_end_time'],
lock['reason']] for lock in locks],
headers=['ID', 'Pair', 'Until', 'Reason'],
tablefmt='simple')
message = f"<pre>{escape(message)}</pre>"
logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _delete_locks(self, update: Update, context: CallbackContext) -> None:
@@ -870,9 +908,17 @@ class Telegram(RPCHandler):
"""
try:
edge_pairs = self._rpc._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)
if not edge_pairs:
message = '<b>Edge only validated following pairs:</b>'
self._send_msg(message, parse_mode=ParseMode.HTML)
for chunk in chunks(edge_pairs, 25):
edge_pairs_tab = tabulate(chunk, headers='keys', tablefmt='simple')
message = (f'<b>Edge only validated following pairs:</b>\n'
f'<pre>{edge_pairs_tab}</pre>')
self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e))

View File

@@ -45,17 +45,21 @@ class Webhook(RPCHandler):
""" Send a message to telegram channel """
try:
if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
if msg['type'] == RPCMessageType.BUY:
valuedict = self._config['webhook'].get('webhookbuy', None)
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
elif msg['type'] == RPCMessageType.BUY_CANCEL:
valuedict = self._config['webhook'].get('webhookbuycancel', None)
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
elif msg['type'] == RPCMessageType.BUY_FILL:
valuedict = self._config['webhook'].get('webhookbuyfill', None)
elif msg['type'] == RPCMessageType.SELL:
valuedict = self._config['webhook'].get('webhooksell', None)
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
elif msg['type'] == RPCMessageType.SELL_FILL:
valuedict = self._config['webhook'].get('webhooksellfill', None)
elif msg['type'] == RPCMessageType.SELL_CANCEL:
valuedict = self._config['webhook'].get('webhooksellcancel', None)
elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION,
RPCMessageType.STARTUP_NOTIFICATION,
RPCMessageType.WARNING_NOTIFICATION):
elif msg['type'] in (RPCMessageType.STATUS,
RPCMessageType.STARTUP,
RPCMessageType.WARNING):
valuedict = self._config['webhook'].get('webhookstatus', None)
else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))