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

@@ -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"