Merge branch 'develop' into feature_keyval_storage

Update rpc/telegram to use MAX_MESSAGE_LENGTH.
This commit is contained in:
eSeR1805
2022-08-11 11:26:45 +03:00
109 changed files with 16046 additions and 13336 deletions

View File

@@ -194,11 +194,11 @@ class OrderSchema(BaseModel):
pair: str
order_id: str
status: str
remaining: float
remaining: Optional[float]
amount: float
safe_price: float
cost: float
filled: float
filled: Optional[float]
ft_order_side: str
order_type: str
is_open: bool
@@ -283,6 +283,7 @@ class OpenTradeSchema(TradeSchema):
class TradeResponse(BaseModel):
trades: List[TradeSchema]
trades_count: int
offset: int
total_trades: int
@@ -324,11 +325,13 @@ class ForceEnterPayload(BaseModel):
ordertype: Optional[OrderTypeValues]
stakeamount: Optional[float]
entry_tag: Optional[str]
leverage: Optional[float]
class ForceExitPayload(BaseModel):
tradeid: str
ordertype: Optional[OrderTypeValues]
amount: Optional[float]
class BlacklistPayload(BaseModel):

View File

@@ -37,7 +37,8 @@ logger = logging.getLogger(__name__)
# 2.14: Add entry/exit orders to trade response
# 2.15: Add backtest history endpoints
# 2.16: Additional daily metrics
API_VERSION = 2.16
# 2.17: Forceentry - leverage, partial force_exit
API_VERSION = 2.17
# Public API, requires no auth.
router_public = APIRouter()
@@ -142,12 +143,11 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
@router.post('/forcebuy', response_model=ForceEnterResponse, tags=['trading'])
def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None
stake_amount = payload.stakeamount if payload.stakeamount else None
entry_tag = payload.entry_tag if payload.entry_tag else 'force_entry'
trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side,
order_type=ordertype, stake_amount=stake_amount,
enter_tag=entry_tag)
order_type=ordertype, stake_amount=payload.stakeamount,
enter_tag=payload.entry_tag or 'force_entry',
leverage=payload.leverage)
if trade:
return ForceEnterResponse.parse_obj(trade.to_json())
@@ -161,7 +161,7 @@ def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
@router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None
return rpc._rpc_force_exit(payload.tradeid, ordertype)
return rpc._rpc_force_exit(payload.tradeid, ordertype, amount=payload.amount)
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
@@ -282,7 +282,7 @@ def get_strategy(strategy: str, config=Depends(get_config)):
def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Optional[str] = None,
candletype: Optional[CandleType] = None, config=Depends(get_config)):
dh = get_datahandler(config['datadir'], config.get('dataformat_ohlcv', None))
dh = get_datahandler(config['datadir'], config.get('dataformat_ohlcv'))
trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
pair_interval = dh.ohlcv_get_available_data(config['datadir'], trading_mode)

View File

@@ -18,9 +18,9 @@ def get_rpc_optional() -> Optional[RPC]:
def get_rpc() -> Optional[Iterator[RPC]]:
_rpc = get_rpc_optional()
if _rpc:
Trade.query.session.rollback()
Trade.rollback()
yield _rpc
Trade.query.session.rollback()
Trade.rollback()
else:
raise RPCException('Bot is not in the correct state')
@@ -37,7 +37,7 @@ def get_exchange(config=Depends(get_config)):
if not ApiServer._exchange:
from freqtrade.resolvers import ExchangeResolver
ApiServer._exchange = ExchangeResolver.load_exchange(
config['exchange']['name'], config)
config['exchange']['name'], config, load_leverage_tiers=False)
return ApiServer._exchange

View File

@@ -1,4 +1,5 @@
from pathlib import Path
from typing import Optional
from fastapi import APIRouter
from fastapi.exceptions import HTTPException
@@ -50,8 +51,12 @@ async def index_html(rest_of_path: str):
filename = uibase / rest_of_path
# It's security relevant to check "relative_to".
# Without this, Directory-traversal is possible.
media_type: Optional[str] = None
if filename.suffix == '.js':
# Force text/javascript for .js files - Circumvent faulty system configuration
media_type = 'application/javascript'
if filename.is_file() and is_relative_to(filename, uibase):
return FileResponse(str(filename))
return FileResponse(str(filename), media_type=media_type)
index_file = uibase / 'index.html'
if not index_file.is_file():

View File

@@ -12,6 +12,7 @@ from pycoingecko import CoinGeckoAPI
from requests.exceptions import RequestException
from freqtrade.constants import SUPPORTED_FIAT
from freqtrade.mixins.logging_mixin import LoggingMixin
logger = logging.getLogger(__name__)
@@ -27,7 +28,7 @@ coingecko_mapping = {
}
class CryptoToFiatConverter:
class CryptoToFiatConverter(LoggingMixin):
"""
Main class to initiate Crypto to FIAT.
This object contains a list of pair Crypto, FIAT
@@ -54,6 +55,7 @@ class CryptoToFiatConverter:
# Timeout: 6h
self._pair_price: TTLCache = TTLCache(maxsize=500, ttl=6 * 60 * 60)
LoggingMixin.__init__(self, logger, 3600)
self._load_cryptomap()
def _load_cryptomap(self) -> None:
@@ -177,7 +179,9 @@ class CryptoToFiatConverter:
if not _gekko_id:
# return 0 for unsupported stake currencies (fiat-convert should not break the bot)
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
self.log_once(
f"unsupported crypto-symbol {crypto_symbol.upper()} - returning 0.0",
logger.warning)
return 0.0
try:

View File

@@ -97,7 +97,7 @@ class RPC:
"""
self._freqtrade = freqtrade
self._config: Dict[str, Any] = freqtrade.config
if self._config.get('fiat_display_currency', None):
if self._config.get('fiat_display_currency'):
self._fiat_converter = CryptoToFiatConverter()
@staticmethod
@@ -201,7 +201,7 @@ class RPC:
trade_dict = trade.to_json()
trade_dict.update(dict(
close_profit=trade.close_profit if trade.close_profit is not None else None,
close_profit=trade.close_profit if not trade.is_open else None,
current_rate=current_rate,
current_profit=current_profit, # Deprecated
current_profit_pct=round(current_profit * 100, 2), # Deprecated
@@ -365,6 +365,7 @@ class RPC:
return {
"trades": output,
"trades_count": len(output),
"offset": offset,
"total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(),
}
@@ -379,7 +380,7 @@ class RPC:
return 'losses'
else:
return 'draws'
trades: List[Trade] = Trade.get_trades([Trade.is_open.is_(False)])
trades: List[Trade] = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False)
# Sell reason
exit_reasons = {}
for trade in trades:
@@ -407,7 +408,8 @@ class RPC:
""" Returns cumulative profit statistics """
trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) |
Trade.is_open.is_(True))
trades: List[Trade] = Trade.get_trades(trade_filter).order_by(Trade.id).all()
trades: List[Trade] = Trade.get_trades(
trade_filter, include_orders=False).order_by(Trade.id).all()
profit_all_coin = []
profit_all_ratio = []
@@ -429,14 +431,15 @@ class RPC:
if not trade.is_open:
profit_ratio = trade.close_profit
profit_closed_coin.append(trade.close_profit_abs)
profit_abs = trade.close_profit_abs
profit_closed_coin.append(profit_abs)
profit_closed_ratio.append(profit_ratio)
if trade.close_profit >= 0:
winning_trades += 1
winning_profit += trade.close_profit_abs
winning_profit += profit_abs
else:
losing_trades += 1
losing_profit += trade.close_profit_abs
losing_profit += profit_abs
else:
# Get current rate
try:
@@ -445,10 +448,10 @@ class RPC:
except (PricingError, ExchangeError):
current_rate = NAN
profit_ratio = trade.calc_profit_ratio(rate=current_rate)
profit_abs = trade.calc_profit(
rate=trade.close_rate or current_rate) + trade.realized_profit
profit_all_coin.append(
trade.calc_profit(rate=trade.close_rate or current_rate)
)
profit_all_coin.append(profit_abs)
profit_all_ratio.append(profit_ratio)
best_pair = Trade.get_best_pair(start_date)
@@ -564,7 +567,7 @@ class RPC:
else:
try:
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
rate = tickers.get(pair, {}).get('last', None)
rate = tickers.get(pair, {}).get('last')
if rate:
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
rate = 1.0 / rate
@@ -657,36 +660,48 @@ class RPC:
return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]:
def __exec_force_exit(self, trade: Trade, ordertype: Optional[str],
amount: Optional[float] = None) -> 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'] == trade.entry_side:
fully_canceled = self._freqtrade.handle_cancel_enter(
trade, order, CANCEL_REASON['FORCE_EXIT'])
if order['side'] == trade.exit_side:
# Cancel order - so it is placed anew with a fresh price.
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_EXIT'])
if not fully_canceled:
# Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_rate(
trade.pair, side='exit', is_short=trade.is_short, refresh=True)
exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT)
order_type = ordertype or self._freqtrade.strategy.order_types.get(
"force_exit", self._freqtrade.strategy.order_types["exit"])
sub_amount: Optional[float] = None
if amount and amount < trade.amount:
# Partial exit ...
min_exit_stake = self._freqtrade.exchange.get_min_pair_stake_amount(
trade.pair, current_rate, trade.stop_loss_pct)
remaining = (trade.amount - amount) * current_rate
if remaining < min_exit_stake:
raise RPCException(f'Remaining amount of {remaining} would be too small.')
sub_amount = amount
self._freqtrade.execute_trade_exit(
trade, current_rate, exit_check, ordertype=order_type,
sub_trade_amt=sub_amount)
def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None, *,
amount: Optional[float] = None) -> Dict[str, str]:
"""
Handler for forceexit <id>.
Sells the given trade at current price
"""
def _exec_force_exit(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'] == trade.entry_side:
fully_canceled = self._freqtrade.handle_cancel_enter(
trade, order, CANCEL_REASON['FORCE_EXIT'])
if order['side'] == trade.exit_side:
# Cancel order - so it is placed anew with a fresh price.
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_EXIT'])
if not fully_canceled:
# Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_rate(
trade.pair, side='exit', is_short=trade.is_short, refresh=True)
exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT)
order_type = ordertype or self._freqtrade.strategy.order_types.get(
"force_exit", self._freqtrade.strategy.order_types["exit"])
self._freqtrade.execute_trade_exit(
trade, current_rate, exit_check, ordertype=order_type)
# ---- EOF def _exec_forcesell ----
if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running')
@@ -695,7 +710,7 @@ class RPC:
if trade_id == 'all':
# Execute sell for all open orders
for trade in Trade.get_open_trades():
_exec_force_exit(trade)
self.__exec_force_exit(trade, ordertype)
Trade.commit()
self._freqtrade.wallets.update()
return {'result': 'Created sell orders for all open trades.'}
@@ -708,7 +723,7 @@ class RPC:
logger.warning('force_exit: Invalid argument received')
raise RPCException('invalid argument')
_exec_force_exit(trade)
self.__exec_force_exit(trade, ordertype, amount)
Trade.commit()
self._freqtrade.wallets.update()
return {'result': f'Created sell order for trade {trade_id}.'}
@@ -717,7 +732,8 @@ class RPC:
order_type: Optional[str] = None,
order_side: SignalDirection = SignalDirection.LONG,
stake_amount: Optional[float] = None,
enter_tag: Optional[str] = 'force_entry') -> Optional[Trade]:
enter_tag: Optional[str] = 'force_entry',
leverage: Optional[float] = None) -> Optional[Trade]:
"""
Handler for forcebuy <asset> <price>
Buys a pair trade at the given or current price
@@ -759,6 +775,7 @@ class RPC:
ordertype=order_type, trade=trade,
is_short=is_short,
enter_tag=enter_tag,
leverage_=leverage,
):
Trade.commit()
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
@@ -893,7 +910,7 @@ class RPC:
lock.active = False
lock.lock_end_time = datetime.now(timezone.utc)
PairLock.query.session.commit()
Trade.commit()
return self._rpc_locks()

View File

@@ -2,6 +2,7 @@
This module contains class to manage RPC communications (Telegram, API, ...)
"""
import logging
from collections import deque
from typing import Any, Dict, List
from freqtrade.enums import RPCMessageType
@@ -77,6 +78,17 @@ class RPCManager:
except NotImplementedError:
logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.")
def process_msg_queue(self, queue: deque) -> None:
"""
Process all messages in the queue.
"""
while queue:
msg = queue.popleft()
self.send_msg({
'type': RPCMessageType.STRATEGY_MSG,
'msg': msg,
})
def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None:
if config['dry_run']:
self.send_msg({

View File

@@ -16,8 +16,8 @@ from typing import Any, Callable, Dict, List, Optional, Union
import arrow
from tabulate import tabulate
from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton,
ParseMode, ReplyKeyboardMarkup, Update)
from telegram import (MAX_MESSAGE_LENGTH, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup,
KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update)
from telegram.error import BadRequest, NetworkError, TelegramError
from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater
from telegram.utils.helpers import escape_markdown
@@ -35,8 +35,6 @@ logger = logging.getLogger(__name__)
logger.debug('Included module rpc.telegram ...')
MAX_TELEGRAM_MESSAGE_LENGTH = 4096
@dataclass
class TimeunitMappings:
@@ -72,7 +70,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
)
return wrapper
# Rollback session to avoid getting data stored in a transaction.
Trade.query.session.rollback()
Trade.rollback()
logger.debug(
'Executing handler: %s for chat_id: %s',
command_handler.__name__,
@@ -244,6 +242,22 @@ class Telegram(RPCHandler):
"""
return f"{msg['exchange']}{' (dry)' if self._config['dry_run'] else ''}"
def _add_analyzed_candle(self, pair: str) -> str:
candle_val = self._config['telegram'].get(
'notification_settings', {}).get('show_candle', 'off')
if candle_val != 'off':
if candle_val == 'ohlc':
analyzed_df, _ = self._rpc._freqtrade.dataprovider.get_analyzed_dataframe(
pair, self._config['timeframe'])
candle = analyzed_df.iloc[-1].squeeze() if len(analyzed_df) > 0 else None
if candle is not None:
return (
f"*Candle OHLC*: `{candle['open']}, {candle['high']}, "
f"{candle['low']}, {candle['close']}`\n"
)
return ''
def _format_entry_msg(self, msg: Dict[str, Any]) -> str:
if self._rpc._fiat_converter:
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
@@ -259,8 +273,9 @@ class Telegram(RPCHandler):
f"{emoji} *{self._exchange_from_msg(msg)}:*"
f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}"
f" (#{msg['trade_id']})\n"
)
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag', None) else ""
)
message += self._add_analyzed_candle(msg['pair'])
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else ""
message += f"*Amount:* `{msg['amount']:.8f}`\n"
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0:
message += f"*Leverage:* `{msg['leverage']}`\n"
@@ -273,7 +288,7 @@ class Telegram(RPCHandler):
message += f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}"
if msg.get('fiat_currency', None):
if msg.get('fiat_currency'):
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
message += ")`"
@@ -289,7 +304,7 @@ class Telegram(RPCHandler):
msg['enter_tag'] = msg['enter_tag'] if "enter_tag" in msg.keys() else None
msg['emoji'] = self._get_sell_emoji(msg)
msg['leverage_text'] = (f"*Leverage:* `{msg['leverage']:.1f}`\n"
if msg.get('leverage', None) and msg.get('leverage', 1.0) != 1.0
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0
else "")
# Check if all sell properties are available.
@@ -299,19 +314,36 @@ class Telegram(RPCHandler):
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
msg['profit_extra'] = (
f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}"
f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']})")
f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}")
else:
msg['profit_extra'] = ''
msg['profit_extra'] = (
f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}"
f"{msg['profit_extra']})")
is_fill = msg['type'] == RPCMessageType.EXIT_FILL
is_sub_trade = msg.get('sub_trade')
is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit')
profit_prefix = ('Sub ' if is_sub_profit
else 'Cumulative ') if is_sub_trade else ''
cp_extra = ''
if is_sub_profit and is_sub_trade:
if self._rpc._fiat_converter:
cp_fiat = self._rpc._fiat_converter.convert_amount(
msg['cumulative_profit'], msg['stake_currency'], msg['fiat_currency'])
cp_extra = f" / {cp_fiat:.3f} {msg['fiat_currency']}"
else:
cp_extra = ''
cp_extra = f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " \
f"{msg['stake_currency']}{cp_extra}`)\n"
message = (
f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
f"{self._add_analyzed_candle(msg['pair'])}"
f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* "
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
f"{cp_extra}"
f"*Enter Tag:* `{msg['enter_tag']}`\n"
f"*Exit Reason:* `{msg['exit_reason']}`\n"
f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n"
f"*Direction:* `{msg['direction']}`\n"
f"{msg['leverage_text']}"
f"*Amount:* `{msg['amount']:.8f}`\n"
@@ -319,11 +351,25 @@ class Telegram(RPCHandler):
)
if msg['type'] == RPCMessageType.EXIT:
message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
f"*Close Rate:* `{msg['limit']:.8f}`")
f"*Exit Rate:* `{msg['limit']:.8f}`")
elif msg['type'] == RPCMessageType.EXIT_FILL:
message += f"*Close Rate:* `{msg['close_rate']:.8f}`"
message += f"*Exit Rate:* `{msg['close_rate']:.8f}`"
if msg.get('sub_trade'):
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
rem = round_coin_value(msg['stake_amount'], msg['stake_currency'])
message += f"\n*Remaining:* `({rem}"
if msg.get('fiat_currency', None):
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
message += ")`"
else:
message += f"\n*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`"
return message
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
@@ -336,7 +382,8 @@ class Telegram(RPCHandler):
elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL):
msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit'
message = (f"\N{WARNING SIGN} *{self._exchange_from_msg(msg)}:* "
f"Cancelling {msg['message_side']} Order for {msg['pair']} "
f"Cancelling {'partial ' if msg.get('sub_trade') else ''}"
f"{msg['message_side']} Order for {msg['pair']} "
f"(#{msg['trade_id']}). Reason: {msg['reason']}.")
elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
@@ -359,7 +406,8 @@ class Telegram(RPCHandler):
elif msg_type == RPCMessageType.STARTUP:
message = f"{msg['status']}"
elif msg_type == RPCMessageType.STRATEGY_MSG:
message = f"{msg['msg']}"
else:
raise NotImplementedError(f"Unknown message type: {msg_type}")
return message
@@ -406,54 +454,63 @@ class Telegram(RPCHandler):
else:
return "\N{CROSS MARK}"
def _prepare_entry_details(self, filled_orders: List, quote_currency: str, is_open: bool):
def _prepare_order_details(self, filled_orders: List, quote_currency: str, is_open: bool):
"""
Prepare details of trade with entry adjustment enabled
"""
lines: List[str] = []
lines_detail: List[str] = []
if len(filled_orders) > 0:
first_avg = filled_orders[0]["safe_price"]
for x, order in enumerate(filled_orders):
if not order['ft_is_entry'] or order['is_open'] is True:
lines: List[str] = []
if order['is_open'] is True:
continue
wording = 'Entry' if order['ft_is_entry'] else 'Exit'
cur_entry_datetime = arrow.get(order["order_filled_date"])
cur_entry_amount = order["amount"]
cur_entry_amount = order["filled"] or order["amount"]
cur_entry_average = order["safe_price"]
lines.append(" ")
if x == 0:
lines.append(f"*Entry #{x+1}:*")
lines.append(f"*{wording} #{x+1}:*")
lines.append(
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
lines.append(f"*Average Entry Price:* {cur_entry_average}")
f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
lines.append(f"*Average Price:* {cur_entry_average}")
else:
sumA = 0
sumB = 0
for y in range(x):
sumA += (filled_orders[y]["amount"] * filled_orders[y]["safe_price"])
sumB += filled_orders[y]["amount"]
amount = filled_orders[y]["filled"] or filled_orders[y]["amount"]
sumA += amount * filled_orders[y]["safe_price"]
sumB += amount
prev_avg_price = sumA / sumB
# TODO: This calculation ignores fees.
price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg)
minus_on_entry = 0
if prev_avg_price:
minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price
dur_entry = cur_entry_datetime - arrow.get(
filled_orders[x - 1]["order_filled_date"])
days = dur_entry.days
hours, remainder = divmod(dur_entry.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
lines.append(f"*Entry #{x+1}:* at {minus_on_entry:.2%} avg profit")
lines.append(f"*{wording} #{x+1}:* at {minus_on_entry:.2%} avg profit")
if is_open:
lines.append("({})".format(cur_entry_datetime
.humanize(granularity=["day", "hour", "minute"])))
lines.append(
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
lines.append(f"*Average Entry Price:* {cur_entry_average} "
f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
lines.append(f"*Average {wording} Price:* {cur_entry_average} "
f"({price_to_1st_entry:.2%} from 1st entry rate)")
lines.append(f"*Order filled at:* {order['order_filled_date']}")
lines.append(f"({days}d {hours}h {minutes}m {seconds}s from previous entry)")
return lines
lines.append(f"*Order filled:* {order['order_filled_date']}")
# TODO: is this really useful?
# dur_entry = cur_entry_datetime - arrow.get(
# filled_orders[x - 1]["order_filled_date"])
# days = dur_entry.days
# hours, remainder = divmod(dur_entry.seconds, 3600)
# minutes, seconds = divmod(remainder, 60)
# lines.append(
# f"({days}d {hours}h {minutes}m {seconds}s from previous {wording.lower()})")
lines_detail.append("\n".join(lines))
return lines_detail
@authorized_only
def _status(self, update: Update, context: CallbackContext) -> None:
@@ -468,7 +525,14 @@ class Telegram(RPCHandler):
if context.args and 'table' in context.args:
self._status_table(update, context)
return
else:
self._status_msg(update, context)
def _status_msg(self, update: Update, context: CallbackContext) -> None:
"""
handler for `/status` and `/status <id>`.
"""
try:
# Check if there's at least one numerical ID provided.
@@ -480,14 +544,13 @@ class Telegram(RPCHandler):
results = self._rpc._rpc_trade_status(trade_ids=trade_ids)
position_adjust = self._config.get('position_adjustment_enable', False)
max_entries = self._config.get('max_entry_position_adjustment', -1)
messages = []
for r in results:
r['open_date_hum'] = arrow.get(r['open_date']).humanize()
r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']])
r['exit_reason'] = r.get('exit_reason', "")
lines = [
"*Trade ID:* `{trade_id}`" +
("` (since {open_date_hum})`" if r['is_open'] else ""),
(" `(since {open_date_hum})`" if r['is_open'] else ""),
"*Current Pair:* {pair}",
"*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"),
"*Leverage:* `{leverage}`" if r.get('leverage') else "",
@@ -511,6 +574,8 @@ class Telegram(RPCHandler):
])
if r['is_open']:
if r.get('realized_profit'):
lines.append("*Realized Profit:* `{realized_profit:.8f}`")
if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
and r['initial_stop_loss_ratio'] is not None):
# Adding initial stoploss only if it is different from stoploss
@@ -523,24 +588,34 @@ class Telegram(RPCHandler):
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
"`({stoploss_current_dist_ratio:.2%})`")
if r['open_order']:
if r['exit_order_status']:
lines.append("*Open Order:* `{open_order}` - `{exit_order_status}`")
else:
lines.append("*Open Order:* `{open_order}`")
lines.append(
"*Open Order:* `{open_order}`"
+ "- `{exit_order_status}`" if r['exit_order_status'] else "")
lines_detail = self._prepare_entry_details(
lines_detail = self._prepare_order_details(
r['orders'], r['quote_currency'], r['is_open'])
lines.extend(lines_detail if lines_detail else "")
# 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)
self.__send_status_msg(lines, r)
except RPCException as e:
self._send_msg(str(e))
def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None:
"""
Send status message.
"""
msg = ''
for line in lines:
if line:
if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH:
msg += line + '\n'
else:
self._send_msg(msg.format(**r))
msg = "*Trade ID:* `{trade_id}` - continued\n" + line + '\n'
self._send_msg(msg.format(**r))
@authorized_only
def _status_table(self, update: Update, context: CallbackContext) -> None:
"""
@@ -843,7 +918,7 @@ class Telegram(RPCHandler):
total_dust_currencies += 1
# Handle overflowing message length
if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH:
if len(output + curr_output) >= MAX_MESSAGE_LENGTH:
self._send_msg(output)
output = curr_output
else:
@@ -1106,7 +1181,7 @@ class Telegram(RPCHandler):
f"({trade['profit_ratio']:.2%}) "
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line
else:
@@ -1141,7 +1216,7 @@ class Telegram(RPCHandler):
f"({trade['profit_ratio']:.2%}) "
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line
else:
@@ -1176,7 +1251,7 @@ class Telegram(RPCHandler):
f"({trade['profit_ratio']:.2%}) "
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line
else:
@@ -1211,7 +1286,7 @@ class Telegram(RPCHandler):
f"({trade['profit']:.2%}) "
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line
else:
@@ -1350,7 +1425,7 @@ class Telegram(RPCHandler):
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:
if len(msgs + msg) + 10 >= MAX_MESSAGE_LENGTH:
# Send message immediately if it would become too long
self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
msgs = msg + '\n'
@@ -1568,9 +1643,9 @@ class Telegram(RPCHandler):
# Filter empty lines using list-comprehension
messages.append("\n".join([line for line in lines if line]))
for msg in messages:
if len(msg) > MAX_TELEGRAM_MESSAGE_LENGTH:
if len(msg) > MAX_MESSAGE_LENGTH:
msg = "Message dropped because length exceeds "
msg += f"maximum allowed characters: {MAX_TELEGRAM_MESSAGE_LENGTH}"
msg += f"maximum allowed characters: {MAX_MESSAGE_LENGTH}"
logger.warning(msg)
self._send_msg(msg)
else:

View File

@@ -45,21 +45,21 @@ class Webhook(RPCHandler):
try:
whconfig = self._config['webhook']
if msg['type'] in [RPCMessageType.ENTRY]:
valuedict = whconfig.get('webhookentry', None)
valuedict = whconfig.get('webhookentry')
elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
valuedict = whconfig.get('webhookentrycancel', None)
valuedict = whconfig.get('webhookentrycancel')
elif msg['type'] in [RPCMessageType.ENTRY_FILL]:
valuedict = whconfig.get('webhookentryfill', None)
valuedict = whconfig.get('webhookentryfill')
elif msg['type'] == RPCMessageType.EXIT:
valuedict = whconfig.get('webhookexit', None)
valuedict = whconfig.get('webhookexit')
elif msg['type'] == RPCMessageType.EXIT_FILL:
valuedict = whconfig.get('webhookexitfill', None)
valuedict = whconfig.get('webhookexitfill')
elif msg['type'] == RPCMessageType.EXIT_CANCEL:
valuedict = whconfig.get('webhookexitcancel', None)
valuedict = whconfig.get('webhookexitcancel')
elif msg['type'] in (RPCMessageType.STATUS,
RPCMessageType.STARTUP,
RPCMessageType.WARNING):
valuedict = whconfig.get('webhookstatus', None)
valuedict = whconfig.get('webhookstatus')
else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
if not valuedict: