Merge branch 'freqtrade:develop' into develop

This commit is contained in:
Surfer
2022-06-21 14:16:03 -04:00
committed by GitHub
29 changed files with 344 additions and 144 deletions

View File

@@ -221,7 +221,7 @@ def _download_pair_history(pair: str, *,
prepend=prepend)
logger.info(f'({process}) - Download history data for "{pair}", {timeframe}, '
f'{candle_type} and store in {datadir}.'
f'{candle_type} and store in {datadir}. '
f'From {format_ms_time(since_ms) if since_ms else "start"} to '
f'{format_ms_time(until_ms) if until_ms else "now"}'
)

View File

@@ -93,7 +93,7 @@ class Exchange:
:return: None
"""
self._api: ccxt.Exchange
self._api_async: ccxt_async.Exchange
self._api_async: ccxt_async.Exchange = None
self._markets: Dict = {}
self._trading_fees: Dict[str, Any] = {}
self._leverage_tiers: Dict[str, List[Dict]] = {}
@@ -2131,10 +2131,11 @@ class Exchange:
except ccxt.BaseError as e:
raise OperationalException(e) from e
@retrier
def get_market_leverage_tiers(self, symbol) -> List[Dict]:
@retrier_async
async def get_market_leverage_tiers(self, symbol: str) -> Tuple[str, List[Dict]]:
try:
return self._api.fetch_market_leverage_tiers(symbol)
tier = await self._api_async.fetch_market_leverage_tiers(symbol)
return symbol, tier
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
@@ -2168,8 +2169,14 @@ class Exchange:
f"Initializing leverage_tiers for {len(symbols)} markets. "
"This will take about a minute.")
for symbol in sorted(symbols):
tiers[symbol] = self.get_market_leverage_tiers(symbol)
coros = [self.get_market_leverage_tiers(symbol) for symbol in sorted(symbols)]
for input_coro in chunks(coros, 100):
results = self.loop.run_until_complete(
asyncio.gather(*input_coro, return_exceptions=True))
for symbol, res in results:
tiers[symbol] = res
logger.info(f"Done initializing {len(symbols)} markets.")

View File

@@ -3,6 +3,7 @@ import logging
from datetime import datetime
from typing import Dict, List, Optional, Tuple
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange
@@ -24,6 +25,8 @@ class Gateio(Exchange):
_ft_has: Dict = {
"ohlcv_candle_limit": 1000,
"ohlcv_volume_currency": "quote",
"time_in_force_parameter": "timeInForce",
"order_time_in_force": ['gtc', 'ioc'],
"stoploss_order_types": {"limit": "limit"},
"stoploss_on_exchange": True,
}
@@ -40,13 +43,33 @@ class Gateio(Exchange):
]
def validate_ordertypes(self, order_types: Dict) -> None:
super().validate_ordertypes(order_types)
if self.trading_mode != TradingMode.FUTURES:
if any(v == 'market' for k, v in order_types.items()):
raise OperationalException(
f'Exchange {self.name} does not support market orders.')
def _get_params(
self,
side: BuySell,
ordertype: str,
leverage: float,
reduceOnly: bool,
time_in_force: str = 'gtc',
) -> Dict:
params = super()._get_params(
side=side,
ordertype=ordertype,
leverage=leverage,
reduceOnly=reduceOnly,
time_in_force=time_in_force,
)
if ordertype == 'market' and self.trading_mode == TradingMode.FUTURES:
params['type'] = 'market'
param = self._ft_has.get('time_in_force_parameter', '')
params.update({param: 'ioc'})
return params
def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
params: Optional[Dict] = None) -> List:
trades = super().get_trades_for_order(order_id, pair, since, params)
@@ -61,7 +84,8 @@ class Gateio(Exchange):
pair_fees = self._trading_fees.get(pair, {})
if pair_fees:
for idx, trade in enumerate(trades):
if trade.get('fee', {}).get('cost') is None:
fee = trade.get('fee', {})
if fee and fee.get('cost') is None:
takerOrMaker = trade.get('takerOrMaker', 'taker')
if pair_fees.get(takerOrMaker) is not None:
trades[idx]['fee'] = {

View File

@@ -1055,6 +1055,7 @@ class Backtesting:
# Close trade
open_trade_count -= 1
open_trades[pair].remove(t)
LocalTrade.trades_open.remove(t)
self.wallets.update()
# 2. Process entries.
@@ -1078,6 +1079,8 @@ class Backtesting:
open_trade_count += 1
# logger.debug(f"{pair} - Emulate creation of new trade: {trade}.")
open_trades[pair].append(trade)
LocalTrade.add_bt_trade(trade)
self.wallets.update()
for trade in list(open_trades[pair]):
# 3. Process entry orders.
@@ -1085,7 +1088,6 @@ class Backtesting:
if order and self._get_order_filled(order.price, row):
order.close_bt_order(current_time, trade)
trade.open_order_id = None
LocalTrade.add_bt_trade(trade)
self.wallets.update()
# 4. Create exit orders (if any)

View File

@@ -416,6 +416,9 @@ def generate_strategy_stats(pairlist: List[str],
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'],
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
winning_profit = results.loc[results['profit_abs'] > 0, 'profit_abs'].sum()
losing_profit = results.loc[results['profit_abs'] < 0, 'profit_abs'].sum()
profit_factor = winning_profit / abs(losing_profit) if losing_profit else 0.0
backtest_days = (max_date - min_date).days or 1
strat_stats = {
@@ -443,6 +446,7 @@ def generate_strategy_stats(pairlist: List[str],
'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(),
'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(),
'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']),
'profit_factor': profit_factor,
'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
'backtest_start_ts': int(min_date.timestamp() * 1000),
'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT),
@@ -497,8 +501,10 @@ def generate_strategy_stats(pairlist: List[str],
(drawdown_abs, drawdown_start, drawdown_end, high_val, low_val,
max_drawdown) = calculate_max_drawdown(
results, value_col='profit_abs', starting_balance=start_balance)
# max_relative_drawdown = Underwater
(_, _, _, _, _, max_relative_drawdown) = calculate_max_drawdown(
results, value_col='profit_abs', starting_balance=start_balance, relative=True)
strat_stats.update({
'max_drawdown': max_drawdown_legacy, # Deprecated - do not use
'max_drawdown_account': max_drawdown,
@@ -777,6 +783,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
strat_results['stake_currency'])),
('Total profit %', f"{strat_results['profit_total']:.2%}"),
('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'),
('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor'
in strat_results else 'N/A'),
('Trades per day', strat_results['trades_per_day']),
('Avg. daily profit %',
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),

View File

@@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional
from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
UniqueConstraint, desc, func)
from sqlalchemy.orm import Query, relationship
from sqlalchemy.orm import Query, lazyload, relationship
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort
from freqtrade.enums import ExitType, TradingMode
@@ -624,8 +624,8 @@ class LocalTrade():
"""
self.close_rate = rate
self.close_date = self.close_date or datetime.utcnow()
self.close_profit = self.calc_profit_ratio()
self.close_profit_abs = self.calc_profit()
self.close_profit = self.calc_profit_ratio(rate)
self.close_profit_abs = self.calc_profit(rate)
self.is_open = False
self.exit_order_status = 'closed'
self.open_order_id = None
@@ -693,10 +693,9 @@ class LocalTrade():
"""
self.open_trade_value = self._calc_open_trade_value()
def calculate_interest(self, interest_rate: Optional[float] = None) -> Decimal:
def calculate_interest(self) -> Decimal:
"""
:param interest_rate: interest_charge for borrowing this coin(optional).
If interest_rate is not set self.interest_rate will be used
Calculate interest for this trade. Only applicable for Margin trading.
"""
zero = Decimal(0.0)
# If nothing was borrowed
@@ -709,34 +708,26 @@ class LocalTrade():
total_seconds = Decimal((now - open_date).total_seconds())
hours = total_seconds / sec_per_hour or zero
rate = Decimal(interest_rate or self.interest_rate)
rate = Decimal(self.interest_rate)
borrowed = Decimal(self.borrowed)
return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
def _calc_base_close(self, amount: Decimal, rate: Optional[float] = None,
fee: Optional[float] = None) -> Decimal:
def _calc_base_close(self, amount: Decimal, rate: float, fee: float) -> Decimal:
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
fees = close_trade * Decimal(fee or self.fee_close)
close_trade = amount * Decimal(rate)
fees = close_trade * Decimal(fee)
if self.is_short:
return close_trade + fees
else:
return close_trade - fees
def calc_close_trade_value(self, rate: Optional[float] = None,
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
def calc_close_trade_value(self, rate: float) -> float:
"""
Calculate the close_rate including fee
:param fee: fee to use on the close rate (optional).
If rate is not set self.fee will be used
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: Price in BTC of the open trade
Calculate the Trade's close value including fees
:param rate: rate to compare with.
:return: value in stake currency of the open trade
"""
if rate is None and not self.close_rate:
return 0.0
@@ -745,49 +736,38 @@ class LocalTrade():
trading_mode = self.trading_mode or TradingMode.SPOT
if trading_mode == TradingMode.SPOT:
return float(self._calc_base_close(amount, rate, fee))
return float(self._calc_base_close(amount, rate, self.fee_close))
elif (trading_mode == TradingMode.MARGIN):
total_interest = self.calculate_interest(interest_rate)
total_interest = self.calculate_interest()
if self.is_short:
amount = amount + total_interest
return float(self._calc_base_close(amount, rate, fee))
return float(self._calc_base_close(amount, rate, self.fee_close))
else:
# Currency already owned for longs, no need to purchase
return float(self._calc_base_close(amount, rate, fee) - total_interest)
return float(self._calc_base_close(amount, rate, self.fee_close) - total_interest)
elif (trading_mode == TradingMode.FUTURES):
funding_fees = self.funding_fees or 0.0
# Positive funding_fees -> Trade has gained from fees.
# Negative funding_fees -> Trade had to pay the fees.
if self.is_short:
return float(self._calc_base_close(amount, rate, fee)) - funding_fees
return float(self._calc_base_close(amount, rate, self.fee_close)) - funding_fees
else:
return float(self._calc_base_close(amount, rate, fee)) + funding_fees
return float(self._calc_base_close(amount, rate, self.fee_close)) + funding_fees
else:
raise OperationalException(
f"{self.trading_mode.value} trading is not yet available using freqtrade")
def calc_profit(self, rate: Optional[float] = None,
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
def calc_profit(self, rate: float) -> float:
"""
Calculate the absolute profit in stake currency between Close and Open trade
:param fee: fee to use on the close rate (optional).
If fee is not set self.fee will be used
:param rate: close rate to compare with (optional).
If rate is not set self.close_rate will be used
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: profit in stake currency as float
:param rate: close rate to compare with.
:return: profit in stake currency as float
"""
close_trade_value = self.calc_close_trade_value(
rate=(rate or self.close_rate),
fee=(fee or self.fee_close),
interest_rate=(interest_rate or self.interest_rate)
)
close_trade_value = self.calc_close_trade_value(rate)
if self.is_short:
profit = self.open_trade_value - close_trade_value
@@ -795,23 +775,13 @@ class LocalTrade():
profit = close_trade_value - self.open_trade_value
return float(f"{profit:.8f}")
def calc_profit_ratio(self, rate: Optional[float] = None,
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
def calc_profit_ratio(self, rate: float) -> float:
"""
Calculates the profit as ratio (including fee).
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:param fee: fee to use on the close rate (optional).
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:param rate: rate to compare with.
:return: profit ratio as float
"""
close_trade_value = self.calc_close_trade_value(
rate=(rate or self.close_rate),
fee=(fee or self.fee_close),
interest_rate=(interest_rate or self.interest_rate)
)
close_trade_value = self.calc_close_trade_value(rate)
short_close_zero = (self.is_short and close_trade_value == 0.0)
long_close_zero = (not self.is_short and self.open_trade_value == 0.0)
@@ -1145,7 +1115,7 @@ class Trade(_DECL_BASE, LocalTrade):
)
@staticmethod
def get_trades(trade_filter=None) -> Query:
def get_trades(trade_filter=None, include_orders: bool = True) -> Query:
"""
Helper function to query Trades using filters.
NOTE: Not supported in Backtesting.
@@ -1160,9 +1130,14 @@ class Trade(_DECL_BASE, LocalTrade):
if trade_filter is not None:
if not isinstance(trade_filter, list):
trade_filter = [trade_filter]
return Trade.query.filter(*trade_filter)
this_query = Trade.query.filter(*trade_filter)
else:
return Trade.query
this_query = Trade.query
if not include_orders:
# Don't load order relations
# Consider using noload or raiseload instead of lazyload
this_query = this_query.options(lazyload(Trade.orders))
return this_query
@staticmethod
def get_open_order_trades() -> List['Trade']:
@@ -1382,3 +1357,18 @@ class Trade(_DECL_BASE, LocalTrade):
.group_by(Trade.pair) \
.order_by(desc('profit_sum')).first()
return best_pair
@staticmethod
def get_trading_volume(start_date: datetime = datetime.fromtimestamp(0)) -> float:
"""
Get Trade volume based on Orders
NOTE: Not supported in Backtesting.
:returns: Tuple containing (pair, profit_sum)
"""
trading_volume = Order.query.with_entities(
func.sum(Order.cost).label('volume')
).filter(
Order.order_filled_date >= start_date,
Order.status == 'closed'
).scalar()
return trading_volume

View File

@@ -104,6 +104,10 @@ class Profit(BaseModel):
best_pair_profit_ratio: float
winning_trades: int
losing_trades: int
profit_factor: float
max_drawdown: float
max_drawdown_abs: float
trading_volume: Optional[float]
class SellReason(BaseModel):
@@ -279,6 +283,7 @@ class OpenTradeSchema(TradeSchema):
class TradeResponse(BaseModel):
trades: List[TradeSchema]
trades_count: int
offset: int
total_trades: int

View File

@@ -18,6 +18,7 @@ from freqtrade import __version__
from freqtrade.configuration.timerange import TimeRange
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT
from freqtrade.data.history import load_data
from freqtrade.data.metrics import calculate_max_drawdown
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, State,
TradingMode)
from freqtrade.exceptions import ExchangeError, PricingError
@@ -364,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(),
}
@@ -378,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:
@@ -406,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 = []
@@ -415,6 +418,8 @@ class RPC:
durations = []
winning_trades = 0
losing_trades = 0
winning_profit = 0.0
losing_profit = 0.0
for trade in trades:
current_rate: float = 0.0
@@ -430,8 +435,10 @@ class RPC:
profit_closed_ratio.append(profit_ratio)
if trade.close_profit >= 0:
winning_trades += 1
winning_profit += trade.close_profit_abs
else:
losing_trades += 1
losing_profit += trade.close_profit_abs
else:
# Get current rate
try:
@@ -447,6 +454,7 @@ class RPC:
profit_all_ratio.append(profit_ratio)
best_pair = Trade.get_best_pair(start_date)
trading_volume = Trade.get_trading_volume(start_date)
# Prepare data to display
profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
@@ -470,6 +478,21 @@ class RPC:
profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
profit_factor = winning_profit / abs(losing_profit) if losing_profit else float('inf')
trades_df = DataFrame([{'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT),
'profit_abs': trade.close_profit_abs}
for trade in trades if not trade.is_open])
max_drawdown_abs = 0.0
max_drawdown = 0.0
if len(trades_df) > 0:
try:
(max_drawdown_abs, _, _, _, _, max_drawdown) = calculate_max_drawdown(
trades_df, value_col='profit_abs', starting_balance=starting_balance)
except ValueError:
# ValueError if no losing trade.
pass
profit_all_fiat = self._fiat_converter.convert_amount(
profit_all_coin_sum,
stake_currency,
@@ -508,11 +531,15 @@ class RPC:
'best_pair_profit_ratio': best_pair[1] if best_pair else 0,
'winning_trades': winning_trades,
'losing_trades': losing_trades,
'profit_factor': profit_factor,
'max_drawdown': max_drawdown,
'max_drawdown_abs': max_drawdown_abs,
'trading_volume': trading_volume,
}
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
""" Returns current account balance per crypto """
currencies = []
currencies: List[Dict] = []
total = 0.0
try:
tickers = self._freqtrade.exchange.get_tickers(cached=True)
@@ -547,13 +574,12 @@ class RPC:
except (ExchangeError):
logger.warning(f" Could not get rate for pair {coin}.")
continue
total = total + (est_stake or 0)
total = total + est_stake
currencies.append({
'currency': coin,
# TODO: The below can be simplified if we don't assign None to values.
'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,
'free': balance.free,
'balance': balance.total,
'used': balance.used,
'est_stake': est_stake or 0,
'stake': stake_currency,
'side': 'long',
@@ -583,7 +609,6 @@ class RPC:
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
trade_count = len(Trade.get_trades_proxy())
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
@@ -871,7 +896,7 @@ class RPC:
else:
errors[pair] = {
'error_msg': f"Pair {pair} is not in the current blacklist."
}
}
resp = self._rpc_blacklist()
resp['errors'] = errors
return resp

View File

@@ -235,6 +235,14 @@ class Telegram(RPCHandler):
# This can take up to `timeout` from the call to `start_polling`.
self._updater.stop()
def _exchange_from_msg(self, msg: Dict[str, Any]) -> str:
"""
Extracts the exchange name from the given message.
:param msg: The message to extract the exchange name from.
:return: The exchange name.
"""
return f"{msg['exchange']}{' (dry)' if self._config['dry_run'] else ''}"
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(
@@ -247,7 +255,7 @@ class Telegram(RPCHandler):
entry_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['direction'] == 'Long'
else {'enter': 'Short', 'entered': 'Shorted'})
message = (
f"{emoji} *{msg['exchange']}:*"
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"
)
@@ -298,7 +306,7 @@ class Telegram(RPCHandler):
msg['profit_extra'] = ''
is_fill = msg['type'] == RPCMessageType.EXIT_FILL
message = (
f"{msg['emoji']} *{msg['exchange']}:* "
f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
)
if not is_fill and msg.get('analyzed_candle'):
@@ -334,33 +342,33 @@ 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 = ("\N{WARNING SIGN} *{exchange}:* "
"Cancelling {message_side} Order for {pair} (#{trade_id}). "
"Reason: {reason}.".format(**msg))
message = (f"\N{WARNING SIGN} *{self._exchange_from_msg(msg)}:* "
f"Cancelling {msg['message_side']} Order for {msg['pair']} "
f"(#{msg['trade_id']}). Reason: {msg['reason']}.")
elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
message = (
"*Protection* triggered due to {reason}. "
"`{pair}` will be locked until `{lock_end_time}`."
).format(**msg)
f"*Protection* triggered due to {msg['reason']}. "
f"`{msg['pair']}` will be locked until `{msg['lock_end_time']}`."
)
elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL:
message = (
"*Protection* triggered due to {reason}. "
"*All pairs* will be locked until `{lock_end_time}`."
).format(**msg)
f"*Protection* triggered due to {msg['reason']}. "
f"*All pairs* will be locked until `{msg['lock_end_time']}`."
)
elif msg_type == RPCMessageType.STATUS:
message = '*Status:* `{status}`'.format(**msg)
message = f"*Status:* `{msg['status']}`"
elif msg_type == RPCMessageType.WARNING:
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
message = f"\N{WARNING SIGN} *Warning:* `{msg['status']}`"
elif msg_type == RPCMessageType.STARTUP:
message = '{status}'.format(**msg)
message = f"{msg['status']}"
else:
raise NotImplementedError('Unknown message type: {}'.format(msg_type))
raise NotImplementedError(f"Unknown message type: {msg_type}")
return message
def send_msg(self, msg: Dict[str, Any]) -> None:
@@ -730,12 +738,18 @@ class Telegram(RPCHandler):
f"*Total Trade Count:* `{trade_count}`\n"
f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* "
f"`{first_trade_date}`\n"
f"*Latest Trade opened:* `{latest_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_pair_profit_ratio:.2%}`")
markdown_msg += (
f"\n*Avg. Duration:* `{avg_duration}`\n"
f"*Best Performing:* `{best_pair}: {best_pair_profit_ratio:.2%}`\n"
f"*Trading volume:* `{round_coin_value(stats['trading_volume'], stake_cur)}`\n"
f"*Profit factor:* `{stats['profit_factor']:.2f}`\n"
f"*Max Drawdown:* `{stats['max_drawdown']:.2%} "
f"({round_coin_value(stats['max_drawdown_abs'], stake_cur)})`"
)
self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
query=update.callback_query)
@@ -875,7 +889,7 @@ class Telegram(RPCHandler):
:return: None
"""
msg = self._rpc._rpc_start()
self._send_msg('Status: `{status}`'.format(**msg))
self._send_msg(f"Status: `{msg['status']}`")
@authorized_only
def _stop(self, update: Update, context: CallbackContext) -> None:
@@ -887,7 +901,7 @@ class Telegram(RPCHandler):
:return: None
"""
msg = self._rpc._rpc_stop()
self._send_msg('Status: `{status}`'.format(**msg))
self._send_msg(f"Status: `{msg['status']}`")
@authorized_only
def _reload_config(self, update: Update, context: CallbackContext) -> None:
@@ -899,7 +913,7 @@ class Telegram(RPCHandler):
:return: None
"""
msg = self._rpc._rpc_reload_config()
self._send_msg('Status: `{status}`'.format(**msg))
self._send_msg(f"Status: `{msg['status']}`")
@authorized_only
def _stopbuy(self, update: Update, context: CallbackContext) -> None:
@@ -911,7 +925,7 @@ class Telegram(RPCHandler):
:return: None
"""
msg = self._rpc._rpc_stopbuy()
self._send_msg('Status: `{status}`'.format(**msg))
self._send_msg(f"Status: `{msg['status']}`")
@authorized_only
def _force_exit(self, update: Update, context: CallbackContext) -> None:
@@ -1073,9 +1087,9 @@ class Telegram(RPCHandler):
trade_id = int(context.args[0])
msg = self._rpc._rpc_delete(trade_id)
self._send_msg((
'`{result_msg}`\n'
f"`{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))
@@ -1403,7 +1417,7 @@ class Telegram(RPCHandler):
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
"regardless of profit`\n"
"*/fe <trade_id>|all:* `Alias to /forceexit`\n"
"*/fx <trade_id>|all:* `Alias to /forceexit`\n"
f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}"
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
"*/whitelist:* `Show current whitelist` \n"