Merge branch 'develop' into pr/gmatheu/4746
This commit is contained in:
@@ -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):
|
||||
|
@@ -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'])
|
||||
|
@@ -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):
|
||||
"""
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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}'
|
||||
})
|
||||
|
@@ -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))
|
||||
|
||||
|
@@ -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']))
|
||||
|
Reference in New Issue
Block a user