• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

freqtrade / freqtrade / 9394559170

26 Apr 2024 06:36AM UTC coverage: 94.656% (-0.02%) from 94.674%
9394559170

push

github

xmatthias
Loader should be passed as kwarg for clarity

20280 of 21425 relevant lines covered (94.66%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.66
/freqtrade/rpc/rpc.py
1
"""
2
This module contains class to define a RPC communications
3
"""
4
import logging
1✔
5
from abc import abstractmethod
1✔
6
from datetime import date, datetime, timedelta, timezone
1✔
7
from math import isnan
1✔
8
from typing import Any, Dict, Generator, List, Optional, Sequence, Tuple, Union
1✔
9

10
import psutil
1✔
11
from dateutil.relativedelta import relativedelta
1✔
12
from dateutil.tz import tzlocal
1✔
13
from numpy import NAN, inf, int64, mean
1✔
14
from pandas import DataFrame, NaT
1✔
15
from sqlalchemy import func, select
1✔
16

17
from freqtrade import __version__
1✔
18
from freqtrade.configuration.timerange import TimeRange
1✔
19
from freqtrade.constants import CANCEL_REASON, Config
1✔
20
from freqtrade.data.history import load_data
1✔
21
from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown
1✔
22
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, SignalDirection,
1✔
23
                             State, TradingMode)
24
from freqtrade.exceptions import ExchangeError, PricingError
1✔
25
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
1✔
26
from freqtrade.exchange.types import Tickers
1✔
27
from freqtrade.loggers import bufferHandler
1✔
28
from freqtrade.persistence import KeyStoreKeys, KeyValueStore, PairLocks, Trade
1✔
29
from freqtrade.persistence.models import PairLock
1✔
30
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
1✔
31
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
1✔
32
from freqtrade.rpc.rpc_types import RPCSendMsg
1✔
33
from freqtrade.util import decimals_per_coin, dt_now, dt_ts_def, format_date, shorten_date
1✔
34
from freqtrade.util.datetime_helpers import dt_humanize_delta
1✔
35
from freqtrade.wallets import PositionWallet, Wallet
1✔
36

37

38
logger = logging.getLogger(__name__)
1✔
39

40

41
class RPCException(Exception):
1✔
42
    """
43
    Should be raised with a rpc-formatted message in an _rpc_* method
44
    if the required state is wrong, i.e.:
45

46
    raise RPCException('*Status:* `no active trade`')
47
    """
48

49
    def __init__(self, message: str) -> None:
1✔
50
        super().__init__(self)
1✔
51
        self.message = message
1✔
52

53
    def __str__(self):
1✔
54
        return self.message
1✔
55

56
    def __json__(self):
1✔
57
        return {
×
58
            'msg': self.message
59
        }
60

61

62
class RPCHandler:
1✔
63

64
    def __init__(self, rpc: 'RPC', config: Config) -> None:
1✔
65
        """
66
        Initializes RPCHandlers
67
        :param rpc: instance of RPC Helper class
68
        :param config: Configuration object
69
        :return: None
70
        """
71
        self._rpc = rpc
1✔
72
        self._config: Config = config
1✔
73

74
    @property
1✔
75
    def name(self) -> str:
1✔
76
        """ Returns the lowercase name of the implementation """
77
        return self.__class__.__name__.lower()
1✔
78

79
    @abstractmethod
1✔
80
    def cleanup(self) -> None:
1✔
81
        """ Cleanup pending module resources """
82

83
    @abstractmethod
1✔
84
    def send_msg(self, msg: RPCSendMsg) -> None:
1✔
85
        """ Sends a message to all registered rpc modules """
86

87

88
class RPC:
1✔
89
    """
90
    RPC class can be used to have extra feature, like bot data, and access to DB data
91
    """
92
    # Bind _fiat_converter if needed
93
    _fiat_converter: Optional[CryptoToFiatConverter] = None
1✔
94

95
    def __init__(self, freqtrade) -> None:
1✔
96
        """
97
        Initializes all enabled rpc modules
98
        :param freqtrade: Instance of a freqtrade bot
99
        :return: None
100
        """
101
        self._freqtrade = freqtrade
1✔
102
        self._config: Config = freqtrade.config
1✔
103
        if self._config.get('fiat_display_currency'):
1✔
104
            self._fiat_converter = CryptoToFiatConverter()
1✔
105

106
    @staticmethod
1✔
107
    def _rpc_show_config(config, botstate: Union[State, str],
1✔
108
                         strategy_version: Optional[str] = None) -> Dict[str, Any]:
109
        """
110
        Return a dict of config options.
111
        Explicitly does NOT return the full config to avoid leakage of sensitive
112
        information via rpc.
113
        """
114
        val = {
1✔
115
            'version': __version__,
116
            'strategy_version': strategy_version,
117
            'dry_run': config['dry_run'],
118
            'trading_mode': config.get('trading_mode', 'spot'),
119
            'short_allowed': config.get('trading_mode', 'spot') != 'spot',
120
            'stake_currency': config['stake_currency'],
121
            'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
122
            'stake_amount': str(config['stake_amount']),
123
            'available_capital': config.get('available_capital'),
124
            'max_open_trades': (config.get('max_open_trades', 0)
125
                                if config.get('max_open_trades', 0) != float('inf') else -1),
126
            'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {},
127
            'stoploss': config.get('stoploss'),
128
            'stoploss_on_exchange': config.get('order_types',
129
                                               {}).get('stoploss_on_exchange', False),
130
            'trailing_stop': config.get('trailing_stop'),
131
            'trailing_stop_positive': config.get('trailing_stop_positive'),
132
            'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
133
            'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
134
            'unfilledtimeout': config.get('unfilledtimeout'),
135
            'use_custom_stoploss': config.get('use_custom_stoploss'),
136
            'order_types': config.get('order_types'),
137
            'bot_name': config.get('bot_name', 'freqtrade'),
138
            'timeframe': config.get('timeframe'),
139
            'timeframe_ms': timeframe_to_msecs(config['timeframe']
140
                                               ) if 'timeframe' in config else 0,
141
            'timeframe_min': timeframe_to_minutes(config['timeframe']
142
                                                  ) if 'timeframe' in config else 0,
143
            'exchange': config['exchange']['name'],
144
            'strategy': config['strategy'],
145
            'force_entry_enable': config.get('force_entry_enable', False),
146
            'exit_pricing': config.get('exit_pricing', {}),
147
            'entry_pricing': config.get('entry_pricing', {}),
148
            'state': str(botstate),
149
            'runmode': config['runmode'].value,
150
            'position_adjustment_enable': config.get('position_adjustment_enable', False),
151
            'max_entry_position_adjustment': (
152
                config.get('max_entry_position_adjustment', -1)
153
                if config.get('max_entry_position_adjustment') != float('inf')
154
                else -1)
155
        }
156
        return val
1✔
157

158
    def _rpc_trade_status(self, trade_ids: Optional[List[int]] = None) -> List[Dict[str, Any]]:
1✔
159
        """
160
        Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
161
        a remotely exposed function
162
        """
163
        # Fetch open trades
164
        if trade_ids:
1✔
165
            trades: Sequence[Trade] = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all()
1✔
166
        else:
167
            trades = Trade.get_open_trades()
1✔
168

169
        if not trades:
1✔
170
            raise RPCException('no active trade')
1✔
171
        else:
172
            results = []
1✔
173
            for trade in trades:
1✔
174
                current_profit_fiat: Optional[float] = None
1✔
175
                total_profit_fiat: Optional[float] = None
1✔
176

177
                # prepare open orders details
178
                oo_details: Optional[str] = ""
1✔
179
                oo_details_lst = [
1✔
180
                    f'({oo.order_type} {oo.side} rem={oo.safe_remaining:.8f})'
181
                    for oo in trade.open_orders
182
                    if oo.ft_order_side not in ['stoploss']
183
                ]
184
                oo_details = ', '.join(oo_details_lst)
1✔
185

186
                total_profit_abs = 0.0
1✔
187
                total_profit_ratio: Optional[float] = None
1✔
188
                # calculate profit and send message to user
189
                if trade.is_open:
1✔
190
                    try:
1✔
191
                        current_rate = self._freqtrade.exchange.get_rate(
1✔
192
                            trade.pair, side='exit', is_short=trade.is_short, refresh=False)
193
                    except (ExchangeError, PricingError):
1✔
194
                        current_rate = NAN
1✔
195
                    if len(trade.select_filled_orders(trade.entry_side)) > 0:
1✔
196

197
                        current_profit = current_profit_abs = current_profit_fiat = NAN
1✔
198
                        if not isnan(current_rate):
1✔
199
                            prof = trade.calculate_profit(current_rate)
1✔
200
                            current_profit = prof.profit_ratio
1✔
201
                            current_profit_abs = prof.profit_abs
1✔
202
                            total_profit_abs = prof.total_profit
1✔
203
                            total_profit_ratio = prof.total_profit_ratio
1✔
204
                    else:
205
                        current_profit = current_profit_abs = current_profit_fiat = 0.0
1✔
206

207
                else:
208
                    # Closed trade ...
209
                    current_rate = trade.close_rate
1✔
210
                    current_profit = trade.close_profit or 0.0
1✔
211
                    current_profit_abs = trade.close_profit_abs or 0.0
1✔
212

213
                # Calculate fiat profit
214
                if not isnan(current_profit_abs) and self._fiat_converter:
1✔
215
                    current_profit_fiat = self._fiat_converter.convert_amount(
1✔
216
                        current_profit_abs,
217
                        self._freqtrade.config['stake_currency'],
218
                        self._freqtrade.config['fiat_display_currency']
219
                    )
220
                    total_profit_fiat = self._fiat_converter.convert_amount(
1✔
221
                        total_profit_abs,
222
                        self._freqtrade.config['stake_currency'],
223
                        self._freqtrade.config['fiat_display_currency']
224
                    )
225

226
                # Calculate guaranteed profit (in case of trailing stop)
227
                stop_entry = trade.calculate_profit(trade.stop_loss)
1✔
228

229
                stoploss_entry_dist = stop_entry.profit_abs
1✔
230
                stoploss_entry_dist_ratio = stop_entry.profit_ratio
1✔
231

232
                # calculate distance to stoploss
233
                stoploss_current_dist = trade.stop_loss - current_rate
1✔
234
                stoploss_current_dist_ratio = stoploss_current_dist / current_rate
1✔
235

236
                trade_dict = trade.to_json()
1✔
237
                trade_dict.update(dict(
1✔
238
                    close_profit=trade.close_profit if not trade.is_open else None,
239
                    current_rate=current_rate,
240
                    profit_ratio=current_profit,
241
                    profit_pct=round(current_profit * 100, 2),
242
                    profit_abs=current_profit_abs,
243
                    profit_fiat=current_profit_fiat,
244
                    total_profit_abs=total_profit_abs,
245
                    total_profit_fiat=total_profit_fiat,
246
                    total_profit_ratio=total_profit_ratio,
247
                    stoploss_current_dist=stoploss_current_dist,
248
                    stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8),
249
                    stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2),
250
                    stoploss_entry_dist=stoploss_entry_dist,
251
                    stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8),
252
                    open_orders=oo_details
253
                ))
254
                results.append(trade_dict)
1✔
255
            return results
1✔
256

257
    def _rpc_status_table(self, stake_currency: str,
1✔
258
                          fiat_display_currency: str) -> Tuple[List, List, float]:
259
        trades: List[Trade] = Trade.get_open_trades()
1✔
260
        nonspot = self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT
1✔
261
        if not trades:
1✔
262
            raise RPCException('no active trade')
1✔
263
        else:
264
            trades_list = []
1✔
265
            fiat_profit_sum = NAN
1✔
266
            for trade in trades:
1✔
267
                # calculate profit and send message to user
268
                try:
1✔
269
                    current_rate = self._freqtrade.exchange.get_rate(
1✔
270
                        trade.pair, side='exit', is_short=trade.is_short, refresh=False)
271
                except (PricingError, ExchangeError):
1✔
272
                    current_rate = NAN
1✔
273
                    trade_profit = NAN
1✔
274
                    profit_str = f'{NAN:.2%}'
1✔
275
                else:
276
                    if trade.nr_of_successful_entries > 0:
1✔
277
                        profit = trade.calculate_profit(current_rate)
1✔
278
                        trade_profit = profit.profit_abs
1✔
279
                        profit_str = f'{profit.profit_ratio:.2%}'
1✔
280
                    else:
281
                        trade_profit = 0.0
1✔
282
                        profit_str = f'{0.0:.2f}'
1✔
283
                direction_str = ('S' if trade.is_short else 'L') if nonspot else ''
1✔
284
                if self._fiat_converter:
1✔
285
                    fiat_profit = self._fiat_converter.convert_amount(
1✔
286
                        trade_profit,
287
                        stake_currency,
288
                        fiat_display_currency
289
                    )
290
                    if not isnan(fiat_profit):
1✔
291
                        profit_str += f" ({fiat_profit:.2f})"
1✔
292
                        fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \
1✔
293
                            else fiat_profit_sum + fiat_profit
294
                else:
295
                    profit_str += f" ({trade_profit:.2f})"
1✔
296
                    fiat_profit_sum = trade_profit if isnan(fiat_profit_sum) \
1✔
297
                        else fiat_profit_sum + trade_profit
298

299
                active_attempt_side_symbols = [
1✔
300
                    '*' if (oo and oo.ft_order_side == trade.entry_side) else '**'
301
                    for oo in trade.open_orders
302
                ]
303

304
                # example: '*.**.**' trying to enter, exit and exit with 3 different orders
305
                active_attempt_side_symbols_str = '.'.join(active_attempt_side_symbols)
1✔
306

307
                detail_trade = [
1✔
308
                    f'{trade.id} {direction_str}',
309
                    trade.pair + active_attempt_side_symbols_str,
310
                    shorten_date(dt_humanize_delta(trade.open_date_utc)),
311
                    profit_str
312
                ]
313

314
                if self._config.get('position_adjustment_enable', False):
1✔
315
                    max_entry_str = ''
1✔
316
                    if self._config.get('max_entry_position_adjustment', -1) > 0:
1✔
317
                        max_entry_str = f"/{self._config['max_entry_position_adjustment'] + 1}"
1✔
318
                    filled_entries = trade.nr_of_successful_entries
1✔
319
                    detail_trade.append(f"{filled_entries}{max_entry_str}")
1✔
320
                trades_list.append(detail_trade)
1✔
321
            profitcol = "Profit"
1✔
322
            if self._fiat_converter:
1✔
323
                profitcol += " (" + fiat_display_currency + ")"
1✔
324
            else:
325
                profitcol += " (" + stake_currency + ")"
1✔
326

327
            columns = [
1✔
328
                'ID L/S' if nonspot else 'ID',
329
                'Pair',
330
                'Since',
331
                profitcol]
332
            if self._config.get('position_adjustment_enable', False):
1✔
333
                columns.append('# Entries')
1✔
334
            return trades_list, columns, fiat_profit_sum
1✔
335

336
    def _rpc_timeunit_profit(
1✔
337
            self, timescale: int,
338
            stake_currency: str, fiat_display_currency: str,
339
            timeunit: str = 'days') -> Dict[str, Any]:
340
        """
341
        :param timeunit: Valid entries are 'days', 'weeks', 'months'
342
        """
343
        start_date = datetime.now(timezone.utc).date()
1✔
344
        if timeunit == 'weeks':
1✔
345
            # weekly
346
            start_date = start_date - timedelta(days=start_date.weekday())  # Monday
1✔
347
        if timeunit == 'months':
1✔
348
            start_date = start_date.replace(day=1)
1✔
349

350
        def time_offset(step: int):
1✔
351
            if timeunit == 'months':
1✔
352
                return relativedelta(months=step)
1✔
353
            return timedelta(**{timeunit: step})
1✔
354

355
        if not (isinstance(timescale, int) and timescale > 0):
1✔
356
            raise RPCException('timescale must be an integer greater than 0')
1✔
357

358
        profit_units: Dict[date, Dict] = {}
1✔
359
        daily_stake = self._freqtrade.wallets.get_total_stake_amount()
1✔
360

361
        for day in range(0, timescale):
1✔
362
            profitday = start_date - time_offset(day)
1✔
363
            # Only query for necessary columns for performance reasons.
364
            trades = Trade.session.execute(
1✔
365
                select(Trade.close_profit_abs)
366
                .filter(Trade.is_open.is_(False),
367
                        Trade.close_date >= profitday,
368
                        Trade.close_date < (profitday + time_offset(1)))
369
                .order_by(Trade.close_date)
370
            ).all()
371

372
            curdayprofit = sum(
1✔
373
                trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
374
            # Calculate this periods starting balance
375
            daily_stake = daily_stake - curdayprofit
1✔
376
            profit_units[profitday] = {
1✔
377
                'amount': curdayprofit,
378
                'daily_stake': daily_stake,
379
                'rel_profit': round(curdayprofit / daily_stake, 8) if daily_stake > 0 else 0,
380
                'trades': len(trades),
381
            }
382

383
        data = [
1✔
384
            {
385
                'date': key,
386
                'abs_profit': value["amount"],
387
                'starting_balance': value["daily_stake"],
388
                'rel_profit': value["rel_profit"],
389
                'fiat_value': self._fiat_converter.convert_amount(
390
                    value['amount'],
391
                    stake_currency,
392
                    fiat_display_currency
393
                ) if self._fiat_converter else 0,
394
                'trade_count': value["trades"],
395
            }
396
            for key, value in profit_units.items()
397
        ]
398
        return {
1✔
399
            'stake_currency': stake_currency,
400
            'fiat_display_currency': fiat_display_currency,
401
            'data': data
402
        }
403

404
    def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict:
1✔
405
        """ Returns the X last trades """
406
        order_by: Any = Trade.id if order_by_id else Trade.close_date.desc()
1✔
407
        if limit:
1✔
408
            trades = Trade.session.scalars(
1✔
409
                Trade.get_trades_query([Trade.is_open.is_(False)])
410
                .order_by(order_by)
411
                .limit(limit)
412
                .offset(offset))
413
        else:
414
            trades = Trade.session.scalars(
1✔
415
                Trade.get_trades_query([Trade.is_open.is_(False)])
416
                .order_by(Trade.close_date.desc()))
417

418
        output = [trade.to_json() for trade in trades]
1✔
419
        total_trades = Trade.session.scalar(
1✔
420
            select(func.count(Trade.id)).filter(Trade.is_open.is_(False)))
421

422
        return {
1✔
423
            "trades": output,
424
            "trades_count": len(output),
425
            "offset": offset,
426
            "total_trades": total_trades,
427
        }
428

429
    def _rpc_stats(self) -> Dict[str, Any]:
1✔
430
        """
431
        Generate generic stats for trades in database
432
        """
433
        def trade_win_loss(trade):
1✔
434
            if trade.close_profit > 0:
1✔
435
                return 'wins'
1✔
436
            elif trade.close_profit < 0:
1✔
437
                return 'losses'
1✔
438
            else:
439
                return 'draws'
×
440
        trades = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False)
1✔
441
        # Duration
442
        dur: Dict[str, List[float]] = {'wins': [], 'draws': [], 'losses': []}
1✔
443
        # Exit reason
444
        exit_reasons = {}
1✔
445
        for trade in trades:
1✔
446
            if trade.exit_reason not in exit_reasons:
1✔
447
                exit_reasons[trade.exit_reason] = {'wins': 0, 'losses': 0, 'draws': 0}
1✔
448
            exit_reasons[trade.exit_reason][trade_win_loss(trade)] += 1
1✔
449

450
            if trade.close_date is not None and trade.open_date is not None:
1✔
451
                trade_dur = (trade.close_date - trade.open_date).total_seconds()
1✔
452
                dur[trade_win_loss(trade)].append(trade_dur)
1✔
453

454
        wins_dur = sum(dur['wins']) / len(dur['wins']) if len(dur['wins']) > 0 else None
1✔
455
        draws_dur = sum(dur['draws']) / len(dur['draws']) if len(dur['draws']) > 0 else None
1✔
456
        losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else None
1✔
457

458
        durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur}
1✔
459
        return {'exit_reasons': exit_reasons, 'durations': durations}
1✔
460

461
    def _rpc_trade_statistics(
1✔
462
            self, stake_currency: str, fiat_display_currency: str,
463
            start_date: Optional[datetime] = None) -> Dict[str, Any]:
464
        """ Returns cumulative profit statistics """
465

466
        start_date = datetime.fromtimestamp(0) if start_date is None else start_date
1✔
467

468
        trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) |
1✔
469
                        Trade.is_open.is_(True))
470
        trades: Sequence[Trade] = Trade.session.scalars(Trade.get_trades_query(
1✔
471
            trade_filter, include_orders=False).order_by(Trade.id)).all()
472

473
        profit_all_coin = []
1✔
474
        profit_all_ratio = []
1✔
475
        profit_closed_coin = []
1✔
476
        profit_closed_ratio = []
1✔
477
        durations = []
1✔
478
        winning_trades = 0
1✔
479
        losing_trades = 0
1✔
480
        winning_profit = 0.0
1✔
481
        losing_profit = 0.0
1✔
482

483
        for trade in trades:
1✔
484
            current_rate: float = 0.0
1✔
485

486
            if trade.close_date:
1✔
487
                durations.append((trade.close_date - trade.open_date).total_seconds())
1✔
488

489
            if not trade.is_open:
1✔
490
                profit_ratio = trade.close_profit or 0.0
1✔
491
                profit_abs = trade.close_profit_abs or 0.0
1✔
492
                profit_closed_coin.append(profit_abs)
1✔
493
                profit_closed_ratio.append(profit_ratio)
1✔
494
                if profit_ratio >= 0:
1✔
495
                    winning_trades += 1
1✔
496
                    winning_profit += profit_abs
1✔
497
                else:
498
                    losing_trades += 1
1✔
499
                    losing_profit += profit_abs
1✔
500
            else:
501
                # Get current rate
502
                try:
1✔
503
                    current_rate = self._freqtrade.exchange.get_rate(
1✔
504
                        trade.pair, side='exit', is_short=trade.is_short, refresh=False)
505
                except (PricingError, ExchangeError):
1✔
506
                    current_rate = NAN
1✔
507
                if isnan(current_rate):
1✔
508
                    profit_ratio = NAN
1✔
509
                    profit_abs = NAN
1✔
510
                else:
511
                    profit = trade.calculate_profit(trade.close_rate or current_rate)
1✔
512

513
                    profit_ratio = profit.profit_ratio
1✔
514
                    profit_abs = profit.total_profit
1✔
515

516
            profit_all_coin.append(profit_abs)
1✔
517
            profit_all_ratio.append(profit_ratio)
1✔
518

519
        closed_trade_count = len([t for t in trades if not t.is_open])
1✔
520

521
        best_pair = Trade.get_best_pair(start_date)
1✔
522
        trading_volume = Trade.get_trading_volume(start_date)
1✔
523

524
        # Prepare data to display
525
        profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
1✔
526
        profit_closed_ratio_mean = float(mean(profit_closed_ratio) if profit_closed_ratio else 0.0)
1✔
527
        profit_closed_ratio_sum = sum(profit_closed_ratio) if profit_closed_ratio else 0.0
1✔
528

529
        profit_closed_fiat = self._fiat_converter.convert_amount(
1✔
530
            profit_closed_coin_sum,
531
            stake_currency,
532
            fiat_display_currency
533
        ) if self._fiat_converter else 0
534

535
        profit_all_coin_sum = round(sum(profit_all_coin), 8)
1✔
536
        profit_all_ratio_mean = float(mean(profit_all_ratio) if profit_all_ratio else 0.0)
1✔
537
        # Doing the sum is not right - overall profit needs to be based on initial capital
538
        profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
1✔
539
        starting_balance = self._freqtrade.wallets.get_starting_balance()
1✔
540
        profit_closed_ratio_fromstart = 0
1✔
541
        profit_all_ratio_fromstart = 0
1✔
542
        if starting_balance:
1✔
543
            profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
1✔
544
            profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
1✔
545

546
        profit_factor = winning_profit / abs(losing_profit) if losing_profit else float('inf')
1✔
547

548
        winrate = (winning_trades / closed_trade_count) if closed_trade_count > 0 else 0
1✔
549

550
        trades_df = DataFrame([{'close_date': format_date(trade.close_date),
1✔
551
                                'close_date_dt': trade.close_date,
552
                                'profit_abs': trade.close_profit_abs}
553
                               for trade in trades if not trade.is_open and trade.close_date])
554

555
        expectancy, expectancy_ratio = calculate_expectancy(trades_df)
1✔
556

557
        max_drawdown_abs = 0.0
1✔
558
        max_drawdown = 0.0
1✔
559
        drawdown_start: Optional[datetime] = None
1✔
560
        drawdown_end: Optional[datetime] = None
1✔
561
        dd_high_val = dd_low_val = 0.0
1✔
562
        if len(trades_df) > 0:
1✔
563
            try:
1✔
564
                (max_drawdown_abs, drawdown_start, drawdown_end, dd_high_val, dd_low_val,
1✔
565
                 max_drawdown) = calculate_max_drawdown(
566
                    trades_df, value_col='profit_abs', date_col='close_date_dt',
567
                    starting_balance=starting_balance)
568
            except ValueError:
1✔
569
                # ValueError if no losing trade.
570
                pass
1✔
571

572
        profit_all_fiat = self._fiat_converter.convert_amount(
1✔
573
            profit_all_coin_sum,
574
            stake_currency,
575
            fiat_display_currency
576
        ) if self._fiat_converter else 0
577

578
        first_date = trades[0].open_date_utc if trades else None
1✔
579
        last_date = trades[-1].open_date_utc if trades else None
1✔
580
        num = float(len(durations) or 1)
1✔
581
        bot_start = KeyValueStore.get_datetime_value(KeyStoreKeys.BOT_START_TIME)
1✔
582
        return {
1✔
583
            'profit_closed_coin': profit_closed_coin_sum,
584
            'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2),
585
            'profit_closed_ratio_mean': profit_closed_ratio_mean,
586
            'profit_closed_percent_sum': round(profit_closed_ratio_sum * 100, 2),
587
            'profit_closed_ratio_sum': profit_closed_ratio_sum,
588
            'profit_closed_ratio': profit_closed_ratio_fromstart,
589
            'profit_closed_percent': round(profit_closed_ratio_fromstart * 100, 2),
590
            'profit_closed_fiat': profit_closed_fiat,
591
            'profit_all_coin': profit_all_coin_sum,
592
            'profit_all_percent_mean': round(profit_all_ratio_mean * 100, 2),
593
            'profit_all_ratio_mean': profit_all_ratio_mean,
594
            'profit_all_percent_sum': round(profit_all_ratio_sum * 100, 2),
595
            'profit_all_ratio_sum': profit_all_ratio_sum,
596
            'profit_all_ratio': profit_all_ratio_fromstart,
597
            'profit_all_percent': round(profit_all_ratio_fromstart * 100, 2),
598
            'profit_all_fiat': profit_all_fiat,
599
            'trade_count': len(trades),
600
            'closed_trade_count': closed_trade_count,
601
            'first_trade_date': format_date(first_date),
602
            'first_trade_humanized': dt_humanize_delta(first_date) if first_date else '',
603
            'first_trade_timestamp': dt_ts_def(first_date, 0),
604
            'latest_trade_date': format_date(last_date),
605
            'latest_trade_humanized': dt_humanize_delta(last_date) if last_date else '',
606
            'latest_trade_timestamp': dt_ts_def(last_date, 0),
607
            'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
608
            'best_pair': best_pair[0] if best_pair else '',
609
            'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0,  # Deprecated
610
            'best_pair_profit_ratio': best_pair[1] if best_pair else 0,
611
            'winning_trades': winning_trades,
612
            'losing_trades': losing_trades,
613
            'profit_factor': profit_factor,
614
            'winrate': winrate,
615
            'expectancy': expectancy,
616
            'expectancy_ratio': expectancy_ratio,
617
            'max_drawdown': max_drawdown,
618
            'max_drawdown_abs': max_drawdown_abs,
619
            'max_drawdown_start': format_date(drawdown_start),
620
            'max_drawdown_start_timestamp': dt_ts_def(drawdown_start),
621
            'max_drawdown_end': format_date(drawdown_end),
622
            'max_drawdown_end_timestamp': dt_ts_def(drawdown_end),
623
            'drawdown_high': dd_high_val,
624
            'drawdown_low': dd_low_val,
625
            'trading_volume': trading_volume,
626
            'bot_start_timestamp': dt_ts_def(bot_start, 0),
627
            'bot_start_date': format_date(bot_start),
628
        }
629

630
    def __balance_get_est_stake(
1✔
631
            self, coin: str, stake_currency: str, amount: float,
632
            balance: Wallet, tickers) -> Tuple[float, float]:
633
        est_stake = 0.0
1✔
634
        est_bot_stake = 0.0
1✔
635
        if coin == stake_currency:
1✔
636
            est_stake = balance.total
1✔
637
            if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
1✔
638
                # in Futures, "total" includes the locked stake, and therefore all positions
639
                est_stake = balance.free
1✔
640
            est_bot_stake = amount
1✔
641
        else:
642
            pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
1✔
643
            rate: Optional[float] = tickers.get(pair, {}).get('last', None)
1✔
644
            if rate:
1✔
645
                if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
1✔
646
                    rate = 1.0 / rate
×
647
                est_stake = rate * balance.total
1✔
648
                est_bot_stake = rate * amount
1✔
649

650
        return est_stake, est_bot_stake
1✔
651

652
    def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
1✔
653
        """ Returns current account balance per crypto """
654
        currencies: List[Dict] = []
1✔
655
        total = 0.0
1✔
656
        total_bot = 0.0
1✔
657
        try:
1✔
658
            tickers: Tickers = self._freqtrade.exchange.get_tickers(cached=True)
1✔
659
        except (ExchangeError):
1✔
660
            raise RPCException('Error getting current tickers.')
1✔
661

662
        open_trades: List[Trade] = Trade.get_open_trades()
1✔
663
        open_assets: Dict[str, Trade] = {t.safe_base_currency: t for t in open_trades}
1✔
664
        self._freqtrade.wallets.update(require_update=False)
1✔
665
        starting_capital = self._freqtrade.wallets.get_starting_balance()
1✔
666
        starting_cap_fiat = self._fiat_converter.convert_amount(
1✔
667
            starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0
668
        coin: str
669
        balance: Wallet
670
        for coin, balance in self._freqtrade.wallets.get_all_balances().items():
1✔
671
            if not balance.total:
1✔
672
                continue
1✔
673

674
            trade = open_assets.get(coin, None)
1✔
675
            is_bot_managed = coin == stake_currency or trade is not None
1✔
676
            trade_amount = trade.amount if trade else 0
1✔
677
            if coin == stake_currency:
1✔
678
                trade_amount = self._freqtrade.wallets.get_available_stake_amount()
1✔
679

680
            try:
1✔
681
                est_stake, est_stake_bot = self.__balance_get_est_stake(
1✔
682
                    coin, stake_currency, trade_amount, balance, tickers)
683
            except ValueError:
×
684
                continue
×
685

686
            total += est_stake
1✔
687

688
            if is_bot_managed:
1✔
689
                total_bot += est_stake_bot
1✔
690
            currencies.append({
1✔
691
                'currency': coin,
692
                'free': balance.free,
693
                'balance': balance.total,
694
                'used': balance.used,
695
                'bot_owned': trade_amount,
696
                'est_stake': est_stake or 0,
697
                'est_stake_bot': est_stake_bot if is_bot_managed else 0,
698
                'stake': stake_currency,
699
                'side': 'long',
700
                'leverage': 1,
701
                'position': 0,
702
                'is_bot_managed': is_bot_managed,
703
                'is_position': False,
704
            })
705
        symbol: str
706
        position: PositionWallet
707
        for symbol, position in self._freqtrade.wallets.get_all_positions().items():
1✔
708
            total += position.collateral
1✔
709
            total_bot += position.collateral
1✔
710

711
            currencies.append({
1✔
712
                'currency': symbol,
713
                'free': 0,
714
                'balance': 0,
715
                'used': 0,
716
                'position': position.position,
717
                'est_stake': position.collateral,
718
                'est_stake_bot': position.collateral,
719
                'stake': stake_currency,
720
                'leverage': position.leverage,
721
                'side': position.side,
722
                'is_bot_managed': True,
723
                'is_position': True
724
            })
725

726
        value = self._fiat_converter.convert_amount(
1✔
727
            total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
728
        value_bot = self._fiat_converter.convert_amount(
1✔
729
            total_bot, stake_currency, fiat_display_currency) if self._fiat_converter else 0
730

731
        trade_count = len(Trade.get_trades_proxy())
1✔
732
        starting_capital_ratio = (total_bot / starting_capital) - 1 if starting_capital else 0.0
1✔
733
        starting_cap_fiat_ratio = (value_bot / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
1✔
734

735
        return {
1✔
736
            'currencies': currencies,
737
            'total': total,
738
            'total_bot': total_bot,
739
            'symbol': fiat_display_currency,
740
            'value': value,
741
            'value_bot': value_bot,
742
            'stake': stake_currency,
743
            'starting_capital': starting_capital,
744
            'starting_capital_ratio': starting_capital_ratio,
745
            'starting_capital_pct': round(starting_capital_ratio * 100, 2),
746
            'starting_capital_fiat': starting_cap_fiat,
747
            'starting_capital_fiat_ratio': starting_cap_fiat_ratio,
748
            'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2),
749
            'trade_count': trade_count,
750
            'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else ''
751
        }
752

753
    def _rpc_start(self) -> Dict[str, str]:
1✔
754
        """ Handler for start """
755
        if self._freqtrade.state == State.RUNNING:
1✔
756
            return {'status': 'already running'}
1✔
757

758
        self._freqtrade.state = State.RUNNING
1✔
759
        return {'status': 'starting trader ...'}
1✔
760

761
    def _rpc_stop(self) -> Dict[str, str]:
1✔
762
        """ Handler for stop """
763
        if self._freqtrade.state == State.RUNNING:
1✔
764
            self._freqtrade.state = State.STOPPED
1✔
765
            return {'status': 'stopping trader ...'}
1✔
766

767
        return {'status': 'already stopped'}
1✔
768

769
    def _rpc_reload_config(self) -> Dict[str, str]:
1✔
770
        """ Handler for reload_config. """
771
        self._freqtrade.state = State.RELOAD_CONFIG
1✔
772
        return {'status': 'Reloading config ...'}
1✔
773

774
    def _rpc_stopentry(self) -> Dict[str, str]:
1✔
775
        """
776
        Handler to stop buying, but handle open trades gracefully.
777
        """
778
        if self._freqtrade.state == State.RUNNING:
1✔
779
            # Set 'max_open_trades' to 0
780
            self._freqtrade.config['max_open_trades'] = 0
1✔
781
            self._freqtrade.strategy.max_open_trades = 0
1✔
782

783
        return {'status': 'No more entries will occur from now. Run /reload_config to reset.'}
1✔
784

785
    def _rpc_reload_trade_from_exchange(self, trade_id: int) -> Dict[str, str]:
1✔
786
        """
787
        Handler for reload_trade_from_exchange.
788
        Reloads a trade from it's orders, should manual interaction have happened.
789
        """
790
        trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
1✔
791
        if not trade:
1✔
792
            raise RPCException(f"Could not find trade with id {trade_id}.")
1✔
793

794
        self._freqtrade.handle_onexchange_order(trade)
1✔
795
        return {'status': 'Reloaded from orders from exchange'}
1✔
796

797
    def __exec_force_exit(self, trade: Trade, ordertype: Optional[str],
1✔
798
                          amount: Optional[float] = None) -> bool:
799
        # Check if there is there are open orders
800
        trade_entry_cancelation_registry = []
1✔
801
        for oo in trade.open_orders:
1✔
802
            trade_entry_cancelation_res = {'order_id': oo.order_id, 'cancel_state': False}
1✔
803
            order = self._freqtrade.exchange.fetch_order(oo.order_id, trade.pair)
1✔
804

805
            if order['side'] == trade.entry_side:
1✔
806
                fully_canceled = self._freqtrade.handle_cancel_enter(
1✔
807
                    trade, order, oo, CANCEL_REASON['FORCE_EXIT'])
808
                trade_entry_cancelation_res['cancel_state'] = fully_canceled
1✔
809
                trade_entry_cancelation_registry.append(trade_entry_cancelation_res)
1✔
810

811
            if order['side'] == trade.exit_side:
1✔
812
                # Cancel order - so it is placed anew with a fresh price.
813
                self._freqtrade.handle_cancel_exit(
1✔
814
                    trade, order, oo, CANCEL_REASON['FORCE_EXIT'])
815

816
        if all(tocr['cancel_state'] is False for tocr in trade_entry_cancelation_registry):
1✔
817
            if trade.has_open_orders:
1✔
818
                # Order cancellation failed, so we can't exit.
819
                return False
×
820
            # Get current rate and execute sell
821
            current_rate = self._freqtrade.exchange.get_rate(
1✔
822
                trade.pair, side='exit', is_short=trade.is_short, refresh=True)
823
            exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT)
1✔
824
            order_type = ordertype or self._freqtrade.strategy.order_types.get(
1✔
825
                "force_exit", self._freqtrade.strategy.order_types["exit"])
826
            sub_amount: Optional[float] = None
1✔
827
            if amount and amount < trade.amount:
1✔
828
                # Partial exit ...
829
                min_exit_stake = self._freqtrade.exchange.get_min_pair_stake_amount(
1✔
830
                    trade.pair, current_rate, trade.stop_loss_pct)
831
                remaining = (trade.amount - amount) * current_rate
1✔
832
                if remaining < min_exit_stake:
1✔
833
                    raise RPCException(f'Remaining amount of {remaining} would be too small.')
×
834
                sub_amount = amount
1✔
835

836
            self._freqtrade.execute_trade_exit(
1✔
837
                trade, current_rate, exit_check, ordertype=order_type,
838
                sub_trade_amt=sub_amount)
839

840
            return True
1✔
841
        return False
×
842

843
    def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None, *,
1✔
844
                        amount: Optional[float] = None) -> Dict[str, str]:
845
        """
846
        Handler for forceexit <id>.
847
        Sells the given trade at current price
848
        """
849

850
        if self._freqtrade.state != State.RUNNING:
1✔
851
            raise RPCException('trader is not running')
1✔
852

853
        with self._freqtrade._exit_lock:
1✔
854
            if trade_id == 'all':
1✔
855
                # Execute exit for all open orders
856
                for trade in Trade.get_open_trades():
1✔
857
                    self.__exec_force_exit(trade, ordertype)
1✔
858
                Trade.commit()
1✔
859
                self._freqtrade.wallets.update()
1✔
860
                return {'result': 'Created exit orders for all open trades.'}
1✔
861

862
            # Query for trade
863
            trade = Trade.get_trades(
1✔
864
                trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ]
865
            ).first()
866
            if not trade:
1✔
867
                logger.warning('force_exit: Invalid argument received')
1✔
868
                raise RPCException('invalid argument')
1✔
869

870
            result = self.__exec_force_exit(trade, ordertype, amount)
1✔
871
            Trade.commit()
1✔
872
            self._freqtrade.wallets.update()
1✔
873
            if not result:
1✔
874
                raise RPCException('Failed to exit trade.')
×
875
            return {'result': f'Created exit order for trade {trade_id}.'}
1✔
876

877
    def _force_entry_validations(self, pair: str, order_side: SignalDirection):
1✔
878
        if not self._freqtrade.config.get('force_entry_enable', False):
1✔
879
            raise RPCException('Force_entry not enabled.')
1✔
880

881
        if self._freqtrade.state != State.RUNNING:
1✔
882
            raise RPCException('trader is not running')
1✔
883

884
        if order_side == SignalDirection.SHORT and self._freqtrade.trading_mode == TradingMode.SPOT:
1✔
885
            raise RPCException("Can't go short on Spot markets.")
1✔
886

887
        if pair not in self._freqtrade.exchange.get_markets(tradable_only=True):
1✔
888
            raise RPCException('Symbol does not exist or market is not active.')
1✔
889
        # Check if pair quote currency equals to the stake currency.
890
        stake_currency = self._freqtrade.config.get('stake_currency')
1✔
891
        if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency:
1✔
892
            raise RPCException(
1✔
893
                f'Wrong pair selected. Only pairs with stake-currency {stake_currency} allowed.')
894

895
    def _rpc_force_entry(self, pair: str, price: Optional[float], *,
1✔
896
                         order_type: Optional[str] = None,
897
                         order_side: SignalDirection = SignalDirection.LONG,
898
                         stake_amount: Optional[float] = None,
899
                         enter_tag: Optional[str] = 'force_entry',
900
                         leverage: Optional[float] = None) -> Optional[Trade]:
901
        """
902
        Handler for forcebuy <asset> <price>
903
        Buys a pair trade at the given or current price
904
        """
905
        self._force_entry_validations(pair, order_side)
1✔
906

907
        # check if valid pair
908

909
        # check if pair already has an open pair
910
        trade: Optional[Trade] = Trade.get_trades(
1✔
911
            [Trade.is_open.is_(True), Trade.pair == pair]).first()
912
        is_short = (order_side == SignalDirection.SHORT)
1✔
913
        if trade:
1✔
914
            is_short = trade.is_short
1✔
915
            if not self._freqtrade.strategy.position_adjustment_enable:
1✔
916
                raise RPCException(f"position for {pair} already open - id: {trade.id}")
1✔
917
            if trade.has_open_orders:
1✔
918
                raise RPCException(f"position for {pair} already open - id: {trade.id} "
1✔
919
                                   f"and has open order {','.join(trade.open_orders_ids)}")
920
        else:
921
            if Trade.get_open_trade_count() >= self._config['max_open_trades']:
1✔
922
                raise RPCException("Maximum number of trades is reached.")
1✔
923

924
        if not stake_amount:
1✔
925
            # gen stake amount
926
            stake_amount = self._freqtrade.wallets.get_trade_stake_amount(
1✔
927
                pair, self._config['max_open_trades'])
928

929
        # execute buy
930
        if not order_type:
1✔
931
            order_type = self._freqtrade.strategy.order_types.get(
1✔
932
                'force_entry', self._freqtrade.strategy.order_types['entry'])
933
        with self._freqtrade._exit_lock:
1✔
934
            if self._freqtrade.execute_entry(pair, stake_amount, price,
1✔
935
                                             ordertype=order_type, trade=trade,
936
                                             is_short=is_short,
937
                                             enter_tag=enter_tag,
938
                                             leverage_=leverage,
939
                                             mode='pos_adjust' if trade else 'initial'
940
                                             ):
941
                Trade.commit()
1✔
942
                trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
1✔
943
                return trade
1✔
944
            else:
945
                raise RPCException(f'Failed to enter position for {pair}.')
1✔
946

947
    def _rpc_cancel_open_order(self, trade_id: int):
1✔
948
        if self._freqtrade.state != State.RUNNING:
1✔
949
            raise RPCException('trader is not running')
×
950
        with self._freqtrade._exit_lock:
1✔
951
            # Query for trade
952
            trade = Trade.get_trades(
1✔
953
                trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ]
954
            ).first()
955
            if not trade:
1✔
956
                logger.warning('cancel_open_order: Invalid trade_id received.')
1✔
957
                raise RPCException('Invalid trade_id.')
1✔
958
            if not trade.has_open_orders:
1✔
959
                logger.warning('cancel_open_order: No open order for trade_id.')
1✔
960
                raise RPCException('No open order for trade_id.')
1✔
961

962
            for open_order in trade.open_orders:
1✔
963
                try:
1✔
964
                    order = self._freqtrade.exchange.fetch_order(open_order.order_id, trade.pair)
1✔
965
                except ExchangeError as e:
1✔
966
                    logger.info(f"Cannot query order for {trade} due to {e}.", exc_info=True)
1✔
967
                    raise RPCException("Order not found.")
1✔
968
                self._freqtrade.handle_cancel_order(
1✔
969
                    order, open_order, trade, CANCEL_REASON['USER_CANCEL'])
970
            Trade.commit()
1✔
971

972
    def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]:
1✔
973
        """
974
        Handler for delete <id>.
975
        Delete the given trade and close eventually existing open orders.
976
        """
977
        with self._freqtrade._exit_lock:
1✔
978
            c_count = 0
1✔
979
            trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
1✔
980
            if not trade:
1✔
981
                logger.warning('delete trade: Invalid argument received')
1✔
982
                raise RPCException('invalid argument')
1✔
983

984
            # Try cancelling regular order if that exists
985
            for open_order in trade.open_orders:
1✔
986
                try:
1✔
987
                    self._freqtrade.exchange.cancel_order(open_order.order_id, trade.pair)
1✔
988
                    c_count += 1
1✔
989
                except (ExchangeError):
1✔
990
                    pass
1✔
991

992
            # cancel stoploss on exchange orders ...
993
            if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange')
1✔
994
                    and trade.has_open_sl_orders):
995

996
                for oslo in trade.open_sl_orders:
1✔
997
                    try:
1✔
998
                        self._freqtrade.exchange.cancel_stoploss_order(oslo.order_id, trade.pair)
1✔
999
                        c_count += 1
1✔
1000
                    except (ExchangeError):
1✔
1001
                        pass
1✔
1002

1003
            trade.delete()
1✔
1004
            self._freqtrade.wallets.update()
1✔
1005
            return {
1✔
1006
                'result': 'success',
1007
                'trade_id': trade_id,
1008
                'result_msg': f'Deleted trade {trade_id}. Closed {c_count} open orders.',
1009
                'cancel_order_count': c_count,
1010
            }
1011

1012
    def _rpc_list_custom_data(self, trade_id: int, key: Optional[str]) -> List[Dict[str, Any]]:
1✔
1013
        # Query for trade
1014
        trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
1✔
1015
        if trade is None:
1✔
1016
            return []
×
1017
        # Query custom_data
1018
        custom_data = []
1✔
1019
        if key:
1✔
1020
            data = trade.get_custom_data(key=key)
×
1021
            if data:
×
1022
                custom_data = [data]
×
1023
        else:
1024
            custom_data = trade.get_all_custom_data()
1✔
1025
        return [
1✔
1026
            {
1027
                'id': data_entry.id,
1028
                'ft_trade_id': data_entry.ft_trade_id,
1029
                'cd_key': data_entry.cd_key,
1030
                'cd_type': data_entry.cd_type,
1031
                'cd_value': data_entry.cd_value,
1032
                'created_at': data_entry.created_at,
1033
                'updated_at': data_entry.updated_at
1034
            }
1035
            for data_entry in custom_data
1036
        ]
1037

1038
    def _rpc_performance(self) -> List[Dict[str, Any]]:
1✔
1039
        """
1040
        Handler for performance.
1041
        Shows a performance statistic from finished trades
1042
        """
1043
        pair_rates = Trade.get_overall_performance()
1✔
1044

1045
        return pair_rates
1✔
1046

1047
    def _rpc_enter_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
1048
        """
1049
        Handler for buy tag performance.
1050
        Shows a performance statistic from finished trades
1051
        """
1052
        return Trade.get_enter_tag_performance(pair)
1✔
1053

1054
    def _rpc_exit_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
1055
        """
1056
        Handler for exit reason performance.
1057
        Shows a performance statistic from finished trades
1058
        """
1059
        return Trade.get_exit_reason_performance(pair)
1✔
1060

1061
    def _rpc_mix_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
1062
        """
1063
        Handler for mix tag (enter_tag + exit_reason) performance.
1064
        Shows a performance statistic from finished trades
1065
        """
1066
        mix_tags = Trade.get_mix_tag_performance(pair)
1✔
1067

1068
        return mix_tags
1✔
1069

1070
    def _rpc_count(self) -> Dict[str, float]:
1✔
1071
        """ Returns the number of trades running """
1072
        if self._freqtrade.state != State.RUNNING:
1✔
1073
            raise RPCException('trader is not running')
1✔
1074

1075
        trades = Trade.get_open_trades()
1✔
1076
        return {
1✔
1077
            'current': len(trades),
1078
            'max': (int(self._freqtrade.config['max_open_trades'])
1079
                    if self._freqtrade.config['max_open_trades'] != float('inf') else -1),
1080
            'total_stake': sum((trade.open_rate * trade.amount) for trade in trades)
1081
        }
1082

1083
    def _rpc_locks(self) -> Dict[str, Any]:
1✔
1084
        """ Returns the  current locks """
1085

1086
        locks = PairLocks.get_pair_locks(None)
1✔
1087
        return {
1✔
1088
            'lock_count': len(locks),
1089
            'locks': [lock.to_json() for lock in locks]
1090
        }
1091

1092
    def _rpc_delete_lock(self, lockid: Optional[int] = None,
1✔
1093
                         pair: Optional[str] = None) -> Dict[str, Any]:
1094
        """ Delete specific lock(s) """
1095
        locks: Sequence[PairLock] = []
1✔
1096

1097
        if pair:
1✔
1098
            locks = PairLocks.get_pair_locks(pair)
1✔
1099
        if lockid:
1✔
1100
            locks = PairLock.session.scalars(select(PairLock).filter(PairLock.id == lockid)).all()
1✔
1101

1102
        for lock in locks:
1✔
1103
            lock.active = False
1✔
1104
            lock.lock_end_time = datetime.now(timezone.utc)
1✔
1105

1106
        Trade.commit()
1✔
1107

1108
        return self._rpc_locks()
1✔
1109

1110
    def _rpc_add_lock(
1✔
1111
            self, pair: str, until: datetime, reason: Optional[str], side: str) -> PairLock:
1112
        lock = PairLocks.lock_pair(
1✔
1113
            pair=pair,
1114
            until=until,
1115
            reason=reason,
1116
            side=side,
1117
        )
1118
        return lock
1✔
1119

1120
    def _rpc_whitelist(self) -> Dict:
1✔
1121
        """ Returns the currently active whitelist"""
1122
        res = {'method': self._freqtrade.pairlists.name_list,
1✔
1123
               'length': len(self._freqtrade.active_pair_whitelist),
1124
               'whitelist': self._freqtrade.active_pair_whitelist
1125
               }
1126
        return res
1✔
1127

1128
    def _rpc_blacklist_delete(self, delete: List[str]) -> Dict:
1✔
1129
        """ Removes pairs from currently active blacklist """
1130
        errors = {}
1✔
1131
        for pair in delete:
1✔
1132
            if pair in self._freqtrade.pairlists.blacklist:
1✔
1133
                self._freqtrade.pairlists.blacklist.remove(pair)
1✔
1134
            else:
1135
                errors[pair] = {
1✔
1136
                    'error_msg': f"Pair {pair} is not in the current blacklist."
1137
                }
1138
        resp = self._rpc_blacklist()
1✔
1139
        resp['errors'] = errors
1✔
1140
        return resp
1✔
1141

1142
    def _rpc_blacklist(self, add: Optional[List[str]] = None) -> Dict:
1✔
1143
        """ Returns the currently active blacklist"""
1144
        errors = {}
1✔
1145
        if add:
1✔
1146
            for pair in add:
1✔
1147
                if pair not in self._freqtrade.pairlists.blacklist:
1✔
1148
                    try:
1✔
1149
                        expand_pairlist([pair], self._freqtrade.exchange.get_markets().keys())
1✔
1150
                        self._freqtrade.pairlists.blacklist.append(pair)
1✔
1151

1152
                    except ValueError:
1✔
1153
                        errors[pair] = {
1✔
1154
                            'error_msg': f'Pair {pair} is not a valid wildcard.'}
1155
                else:
1156
                    errors[pair] = {
1✔
1157
                        'error_msg': f'Pair {pair} already in pairlist.'}
1158

1159
        res = {'method': self._freqtrade.pairlists.name_list,
1✔
1160
               'length': len(self._freqtrade.pairlists.blacklist),
1161
               'blacklist': self._freqtrade.pairlists.blacklist,
1162
               'blacklist_expanded': self._freqtrade.pairlists.expanded_blacklist,
1163
               'errors': errors,
1164
               }
1165
        return res
1✔
1166

1167
    @staticmethod
1✔
1168
    def _rpc_get_logs(limit: Optional[int]) -> Dict[str, Any]:
1✔
1169
        """Returns the last X logs"""
1170
        if limit:
1✔
1171
            buffer = bufferHandler.buffer[-limit:]
1✔
1172
        else:
1173
            buffer = bufferHandler.buffer
1✔
1174
        records = [[format_date(datetime.fromtimestamp(r.created)),
1✔
1175
                   r.created * 1000, r.name, r.levelname,
1176
                   r.message + ('\n' + r.exc_text if r.exc_text else '')]
1177
                   for r in buffer]
1178

1179
        # Log format:
1180
        # [logtime-formatted, logepoch, logger-name, loglevel, message \n + exception]
1181
        # e.g. ["2020-08-27 11:35:01", 1598520901097.9397,
1182
        #       "freqtrade.worker", "INFO", "Starting worker develop"]
1183

1184
        return {'log_count': len(records), 'logs': records}
1✔
1185

1186
    def _rpc_edge(self) -> List[Dict[str, Any]]:
1✔
1187
        """ Returns information related to Edge """
1188
        if not self._freqtrade.edge:
1✔
1189
            raise RPCException('Edge is not enabled.')
1✔
1190
        return self._freqtrade.edge.accepted_pairs()
1✔
1191

1192
    @staticmethod
1✔
1193
    def _convert_dataframe_to_dict(strategy: str, pair: str, timeframe: str, dataframe: DataFrame,
1✔
1194
                                   last_analyzed: datetime) -> Dict[str, Any]:
1195
        has_content = len(dataframe) != 0
1✔
1196
        signals = {
1✔
1197
            'enter_long': 0,
1198
            'exit_long': 0,
1199
            'enter_short': 0,
1200
            'exit_short': 0,
1201
        }
1202
        if has_content:
1✔
1203

1204
            dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].astype(int64) // 1000 // 1000
1✔
1205
            # Move signal close to separate column when signal for easy plotting
1206
            for sig_type in signals.keys():
1✔
1207
                if sig_type in dataframe.columns:
1✔
1208
                    mask = (dataframe[sig_type] == 1)
1✔
1209
                    signals[sig_type] = int(mask.sum())
1✔
1210
                    dataframe.loc[mask, f'_{sig_type}_signal_close'] = dataframe.loc[mask, 'close']
1✔
1211

1212
            # band-aid until this is fixed:
1213
            # https://github.com/pandas-dev/pandas/issues/45836
1214
            datetime_types = ['datetime', 'datetime64', 'datetime64[ns, UTC]']
1✔
1215
            date_columns = dataframe.select_dtypes(include=datetime_types)
1✔
1216
            for date_column in date_columns:
1✔
1217
                # replace NaT with `None`
1218
                dataframe[date_column] = dataframe[date_column].astype(object).replace({NaT: None})
1✔
1219

1220
            dataframe = dataframe.replace({inf: None, -inf: None, NAN: None})
1✔
1221

1222
        res = {
1✔
1223
            'pair': pair,
1224
            'timeframe': timeframe,
1225
            'timeframe_ms': timeframe_to_msecs(timeframe),
1226
            'strategy': strategy,
1227
            'columns': list(dataframe.columns),
1228
            'data': dataframe.values.tolist(),
1229
            'length': len(dataframe),
1230
            'buy_signals': signals['enter_long'],  # Deprecated
1231
            'sell_signals': signals['exit_long'],  # Deprecated
1232
            'enter_long_signals': signals['enter_long'],
1233
            'exit_long_signals': signals['exit_long'],
1234
            'enter_short_signals': signals['enter_short'],
1235
            'exit_short_signals': signals['exit_short'],
1236
            'last_analyzed': last_analyzed,
1237
            'last_analyzed_ts': int(last_analyzed.timestamp()),
1238
            'data_start': '',
1239
            'data_start_ts': 0,
1240
            'data_stop': '',
1241
            'data_stop_ts': 0,
1242
        }
1243
        if has_content:
1✔
1244
            res.update({
1✔
1245
                'data_start': str(dataframe.iloc[0]['date']),
1246
                'data_start_ts': int(dataframe.iloc[0]['__date_ts']),
1247
                'data_stop': str(dataframe.iloc[-1]['date']),
1248
                'data_stop_ts': int(dataframe.iloc[-1]['__date_ts']),
1249
            })
1250
        return res
1✔
1251

1252
    def _rpc_analysed_dataframe(self, pair: str, timeframe: str,
1✔
1253
                                limit: Optional[int]) -> Dict[str, Any]:
1254
        """ Analyzed dataframe in Dict form """
1255

1256
        _data, last_analyzed = self.__rpc_analysed_dataframe_raw(pair, timeframe, limit)
1✔
1257
        return RPC._convert_dataframe_to_dict(self._freqtrade.config['strategy'],
1✔
1258
                                              pair, timeframe, _data, last_analyzed)
1259

1260
    def __rpc_analysed_dataframe_raw(
1✔
1261
        self,
1262
        pair: str,
1263
        timeframe: str,
1264
        limit: Optional[int]
1265
    ) -> Tuple[DataFrame, datetime]:
1266
        """
1267
        Get the dataframe and last analyze from the dataprovider
1268

1269
        :param pair: The pair to get
1270
        :param timeframe: The timeframe of data to get
1271
        :param limit: The amount of candles in the dataframe
1272
        """
1273
        _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(
1✔
1274
            pair, timeframe)
1275
        _data = _data.copy()
1✔
1276

1277
        if limit:
1✔
1278
            _data = _data.iloc[-limit:]
1✔
1279

1280
        return _data, last_analyzed
1✔
1281

1282
    def _ws_all_analysed_dataframes(
1✔
1283
        self,
1284
        pairlist: List[str],
1285
        limit: Optional[int]
1286
    ) -> Generator[Dict[str, Any], None, None]:
1287
        """
1288
        Get the analysed dataframes of each pair in the pairlist.
1289
        If specified, only return the most recent `limit` candles for
1290
        each dataframe.
1291

1292
        :param pairlist: A list of pairs to get
1293
        :param limit: If an integer, limits the size of dataframe
1294
                      If a list of string date times, only returns those candles
1295
        :returns: A generator of dictionaries with the key, dataframe, and last analyzed timestamp
1296
        """
1297
        timeframe = self._freqtrade.config['timeframe']
1✔
1298
        candle_type = self._freqtrade.config.get('candle_type_def', CandleType.SPOT)
1✔
1299

1300
        for pair in pairlist:
1✔
1301
            dataframe, last_analyzed = self.__rpc_analysed_dataframe_raw(pair, timeframe, limit)
1✔
1302

1303
            yield {
1✔
1304
                "key": (pair, timeframe, candle_type),
1305
                "df": dataframe,
1306
                "la": last_analyzed
1307
            }
1308

1309
    def _ws_request_analyzed_df(
1✔
1310
        self,
1311
        limit: Optional[int] = None,
1312
        pair: Optional[str] = None
1313
    ):
1314
        """ Historical Analyzed Dataframes for WebSocket """
1315
        pairlist = [pair] if pair else self._freqtrade.active_pair_whitelist
1✔
1316

1317
        return self._ws_all_analysed_dataframes(pairlist, limit)
1✔
1318

1319
    def _ws_request_whitelist(self):
1✔
1320
        """ Whitelist data for WebSocket """
1321
        return self._freqtrade.active_pair_whitelist
1✔
1322

1323
    @staticmethod
1✔
1324
    def _rpc_analysed_history_full(config: Config, pair: str, timeframe: str,
1✔
1325
                                   exchange) -> Dict[str, Any]:
1326
        timerange_parsed = TimeRange.parse_timerange(config.get('timerange'))
1✔
1327

1328
        from freqtrade.data.converter import trim_dataframe
1✔
1329
        from freqtrade.data.dataprovider import DataProvider
1✔
1330
        from freqtrade.resolvers.strategy_resolver import StrategyResolver
1✔
1331

1332
        strategy = StrategyResolver.load_strategy(config)
1✔
1333
        startup_candles = strategy.startup_candle_count
1✔
1334

1335
        _data = load_data(
1✔
1336
            datadir=config["datadir"],
1337
            pairs=[pair],
1338
            timeframe=timeframe,
1339
            timerange=timerange_parsed,
1340
            data_format=config['dataformat_ohlcv'],
1341
            candle_type=config.get('candle_type_def', CandleType.SPOT),
1342
            startup_candles=startup_candles,
1343
        )
1344
        if pair not in _data:
1✔
1345
            raise RPCException(
1✔
1346
                f"No data for {pair}, {timeframe} in {config.get('timerange')} found.")
1347

1348
        strategy.dp = DataProvider(config, exchange=exchange, pairlists=None)
1✔
1349
        strategy.ft_bot_start()
1✔
1350

1351
        df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair})
1✔
1352
        df_analyzed = trim_dataframe(df_analyzed, timerange_parsed, startup_candles=startup_candles)
1✔
1353

1354
        return RPC._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe,
1✔
1355
                                              df_analyzed.copy(), dt_now())
1356

1357
    def _rpc_plot_config(self) -> Dict[str, Any]:
1✔
1358
        if (self._freqtrade.strategy.plot_config and
1✔
1359
                'subplots' not in self._freqtrade.strategy.plot_config):
1360
            self._freqtrade.strategy.plot_config['subplots'] = {}
1✔
1361
        return self._freqtrade.strategy.plot_config
1✔
1362

1363
    @staticmethod
1✔
1364
    def _rpc_plot_config_with_strategy(config: Config) -> Dict[str, Any]:
1✔
1365

1366
        from freqtrade.resolvers.strategy_resolver import StrategyResolver
1✔
1367
        strategy = StrategyResolver.load_strategy(config)
1✔
1368

1369
        if (strategy.plot_config and 'subplots' not in strategy.plot_config):
1✔
1370
            strategy.plot_config['subplots'] = {}
1✔
1371
        return strategy.plot_config
1✔
1372

1373
    @staticmethod
1✔
1374
    def _rpc_sysinfo() -> Dict[str, Any]:
1✔
1375
        return {
1✔
1376
            "cpu_pct": psutil.cpu_percent(interval=1, percpu=True),
1377
            "ram_pct": psutil.virtual_memory().percent
1378
        }
1379

1380
    def health(self) -> Dict[str, Optional[Union[str, int]]]:
1✔
1381
        last_p = self._freqtrade.last_process
1✔
1382
        res: Dict[str, Union[None, str, int]] = {
1✔
1383
            "last_process": None,
1384
            "last_process_loc": None,
1385
            "last_process_ts": None,
1386
            "bot_start": None,
1387
            "bot_start_loc": None,
1388
            "bot_start_ts": None,
1389
            "bot_startup": None,
1390
            "bot_startup_loc": None,
1391
            "bot_startup_ts": None,
1392
        }
1393

1394
        if last_p is not None:
1✔
1395
            res.update({
×
1396
                "last_process": str(last_p),
1397
                "last_process_loc": format_date(last_p.astimezone(tzlocal())),
1398
                "last_process_ts": int(last_p.timestamp()),
1399
            })
1400

1401
        if (bot_start := KeyValueStore.get_datetime_value(KeyStoreKeys.BOT_START_TIME)):
1✔
1402
            res.update({
1✔
1403
                "bot_start": str(bot_start),
1404
                "bot_start_loc": format_date(bot_start.astimezone(tzlocal())),
1405
                "bot_start_ts": int(bot_start.timestamp()),
1406
            })
1407
        if (bot_startup := KeyValueStore.get_datetime_value(KeyStoreKeys.STARTUP_TIME)):
1✔
1408
            res.update({
1✔
1409
                "bot_startup": str(bot_startup),
1410
                "bot_startup_loc": format_date(bot_startup.astimezone(tzlocal())),
1411
                "bot_startup_ts": int(bot_startup.timestamp()),
1412
            })
1413

1414
        return res
1✔
1415

1416
    def _update_market_direction(self, direction: MarketDirection) -> None:
1✔
1417
        self._freqtrade.strategy.market_direction = direction
1✔
1418

1419
    def _get_market_direction(self) -> MarketDirection:
1✔
1420
        return self._freqtrade.strategy.market_direction
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc