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

freqtrade / freqtrade / 4131167254

pending completion
4131167254

push

github-actions

GitHub
Merge pull request #7983 from stash86/bt-metrics

16866 of 17748 relevant lines covered (95.03%)

0.95 hits per line

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

98.86
/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, Tuple, Union
1✔
9

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

17
from freqtrade import __version__
1✔
18
from freqtrade.configuration.timerange import TimeRange
1✔
19
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT, Config
1✔
20
from freqtrade.data.history import load_data
1✔
21
from freqtrade.data.metrics import calculate_max_drawdown
1✔
22
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, State,
1✔
23
                             TradingMode)
24
from freqtrade.exceptions import ExchangeError, PricingError
1✔
25
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
1✔
26
from freqtrade.loggers import bufferHandler
1✔
27
from freqtrade.misc import decimals_per_coin, shorten_date
1✔
28
from freqtrade.persistence import Order, 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.wallets import PositionWallet, Wallet
1✔
33

34

35
logger = logging.getLogger(__name__)
1✔
36

37

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

43
    raise RPCException('*Status:* `no active trade`')
44
    """
45

46
    def __init__(self, message: str) -> None:
1✔
47
        super().__init__(self)
1✔
48
        self.message = message
1✔
49

50
    def __str__(self):
1✔
51
        return self.message
1✔
52

53
    def __json__(self):
1✔
54
        return {
×
55
            'msg': self.message
56
        }
57

58

59
class RPCHandler:
1✔
60

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

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

76
    @abstractmethod
1✔
77
    def cleanup(self) -> None:
1✔
78
        """ Cleanup pending module resources """
79

80
    @abstractmethod
1✔
81
    def send_msg(self, msg: Dict[str, str]) -> None:
1✔
82
        """ Sends a message to all registered rpc modules """
83

84

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

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

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

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

164
        if not trades:
1✔
165
            raise RPCException('no active trade')
1✔
166
        else:
167
            results = []
1✔
168
            for trade in trades:
1✔
169
                order: Optional[Order] = None
1✔
170
                current_profit_fiat: Optional[float] = None
1✔
171
                if trade.open_order_id:
1✔
172
                    order = trade.select_order_by_order_id(trade.open_order_id)
1✔
173
                # calculate profit and send message to user
174
                if trade.is_open:
1✔
175
                    try:
1✔
176
                        current_rate = self._freqtrade.exchange.get_rate(
1✔
177
                            trade.pair, side='exit', is_short=trade.is_short, refresh=False)
178
                    except (ExchangeError, PricingError):
1✔
179
                        current_rate = NAN
1✔
180
                    if len(trade.select_filled_orders(trade.entry_side)) > 0:
1✔
181
                        current_profit = trade.calc_profit_ratio(
1✔
182
                            current_rate) if not isnan(current_rate) else NAN
183
                        current_profit_abs = trade.calc_profit(
1✔
184
                            current_rate) if not isnan(current_rate) else NAN
185
                    else:
186
                        current_profit = current_profit_abs = current_profit_fiat = 0.0
1✔
187
                else:
188
                    # Closed trade ...
189
                    current_rate = trade.close_rate
1✔
190
                    current_profit = trade.close_profit
1✔
191
                    current_profit_abs = trade.close_profit_abs
1✔
192

193
                # Calculate fiat profit
194
                if not isnan(current_profit_abs) and self._fiat_converter:
1✔
195
                    current_profit_fiat = self._fiat_converter.convert_amount(
1✔
196
                        current_profit_abs,
197
                        self._freqtrade.config['stake_currency'],
198
                        self._freqtrade.config['fiat_display_currency']
199
                    )
200

201
                # Calculate guaranteed profit (in case of trailing stop)
202
                stoploss_entry_dist = trade.calc_profit(trade.stop_loss)
1✔
203
                stoploss_entry_dist_ratio = trade.calc_profit_ratio(trade.stop_loss)
1✔
204
                # calculate distance to stoploss
205
                stoploss_current_dist = trade.stop_loss - current_rate
1✔
206
                stoploss_current_dist_ratio = stoploss_current_dist / current_rate
1✔
207

208
                trade_dict = trade.to_json()
1✔
209
                trade_dict.update(dict(
1✔
210
                    close_profit=trade.close_profit if not trade.is_open else None,
211
                    current_rate=current_rate,
212
                    current_profit=current_profit,  # Deprecated
213
                    current_profit_pct=round(current_profit * 100, 2),  # Deprecated
214
                    current_profit_abs=current_profit_abs,  # Deprecated
215
                    profit_ratio=current_profit,
216
                    profit_pct=round(current_profit * 100, 2),
217
                    profit_abs=current_profit_abs,
218
                    profit_fiat=current_profit_fiat,
219

220
                    stoploss_current_dist=stoploss_current_dist,
221
                    stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8),
222
                    stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2),
223
                    stoploss_entry_dist=stoploss_entry_dist,
224
                    stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8),
225
                    open_order=(
226
                        f'({order.order_type} {order.side} rem={order.safe_remaining:.8f})' if
227
                        order else None
228
                    ),
229
                ))
230
                results.append(trade_dict)
1✔
231
            return results
1✔
232

233
    def _rpc_status_table(self, stake_currency: str,
1✔
234
                          fiat_display_currency: str) -> Tuple[List, List, float]:
235
        trades: List[Trade] = Trade.get_open_trades()
1✔
236
        nonspot = self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT
1✔
237
        if not trades:
1✔
238
            raise RPCException('no active trade')
1✔
239
        else:
240
            trades_list = []
1✔
241
            fiat_profit_sum = NAN
1✔
242
            for trade in trades:
1✔
243
                # calculate profit and send message to user
244
                try:
1✔
245
                    current_rate = self._freqtrade.exchange.get_rate(
1✔
246
                        trade.pair, side='exit', is_short=trade.is_short, refresh=False)
247
                except (PricingError, ExchangeError):
1✔
248
                    current_rate = NAN
1✔
249
                    trade_profit = NAN
1✔
250
                    profit_str = f'{NAN:.2%}'
1✔
251
                else:
252
                    if trade.nr_of_successful_entries > 0:
1✔
253
                        trade_profit = trade.calc_profit(current_rate)
1✔
254
                        profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}'
1✔
255
                    else:
256
                        trade_profit = 0.0
1✔
257
                        profit_str = f'{0.0:.2f}'
1✔
258
                direction_str = ('S' if trade.is_short else 'L') if nonspot else ''
1✔
259
                if self._fiat_converter:
1✔
260
                    fiat_profit = self._fiat_converter.convert_amount(
1✔
261
                        trade_profit,
262
                        stake_currency,
263
                        fiat_display_currency
264
                    )
265
                    if not isnan(fiat_profit):
1✔
266
                        profit_str += f" ({fiat_profit:.2f})"
1✔
267
                        fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \
1✔
268
                            else fiat_profit_sum + fiat_profit
269
                open_order = (trade.select_order_by_order_id(
1✔
270
                    trade.open_order_id) if trade.open_order_id else None)
271

272
                detail_trade = [
1✔
273
                    f'{trade.id} {direction_str}',
274
                    trade.pair + ('*' if (open_order
275
                                  and open_order.ft_order_side == trade.entry_side) else '')
276
                    + ('**' if (open_order and
277
                                open_order.ft_order_side == trade.exit_side is not None) else ''),
278
                    shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
279
                    profit_str
280
                ]
281
                if self._config.get('position_adjustment_enable', False):
1✔
282
                    max_entry_str = ''
1✔
283
                    if self._config.get('max_entry_position_adjustment', -1) > 0:
1✔
284
                        max_entry_str = f"/{self._config['max_entry_position_adjustment'] + 1}"
1✔
285
                    filled_entries = trade.nr_of_successful_entries
1✔
286
                    detail_trade.append(f"{filled_entries}{max_entry_str}")
1✔
287
                trades_list.append(detail_trade)
1✔
288
            profitcol = "Profit"
1✔
289
            if self._fiat_converter:
1✔
290
                profitcol += " (" + fiat_display_currency + ")"
1✔
291

292
            columns = [
1✔
293
                'ID L/S' if nonspot else 'ID',
294
                'Pair',
295
                'Since',
296
                profitcol]
297
            if self._config.get('position_adjustment_enable', False):
1✔
298
                columns.append('# Entries')
1✔
299
            return trades_list, columns, fiat_profit_sum
1✔
300

301
    def _rpc_timeunit_profit(
1✔
302
            self, timescale: int,
303
            stake_currency: str, fiat_display_currency: str,
304
            timeunit: str = 'days') -> Dict[str, Any]:
305
        """
306
        :param timeunit: Valid entries are 'days', 'weeks', 'months'
307
        """
308
        start_date = datetime.now(timezone.utc).date()
1✔
309
        if timeunit == 'weeks':
1✔
310
            # weekly
311
            start_date = start_date - timedelta(days=start_date.weekday())  # Monday
1✔
312
        if timeunit == 'months':
1✔
313
            start_date = start_date.replace(day=1)
1✔
314

315
        def time_offset(step: int):
1✔
316
            if timeunit == 'months':
1✔
317
                return relativedelta(months=step)
1✔
318
            return timedelta(**{timeunit: step})
1✔
319

320
        if not (isinstance(timescale, int) and timescale > 0):
1✔
321
            raise RPCException('timescale must be an integer greater than 0')
1✔
322

323
        profit_units: Dict[date, Dict] = {}
1✔
324
        daily_stake = self._freqtrade.wallets.get_total_stake_amount()
1✔
325

326
        for day in range(0, timescale):
1✔
327
            profitday = start_date - time_offset(day)
1✔
328
            # Only query for necessary columns for performance reasons.
329
            trades = Trade.query.session.query(Trade.close_profit_abs).filter(
1✔
330
                Trade.is_open.is_(False),
331
                Trade.close_date >= profitday,
332
                Trade.close_date < (profitday + time_offset(1))
333
            ).order_by(Trade.close_date).all()
334

335
            curdayprofit = sum(
1✔
336
                trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
337
            # Calculate this periods starting balance
338
            daily_stake = daily_stake - curdayprofit
1✔
339
            profit_units[profitday] = {
1✔
340
                'amount': curdayprofit,
341
                'daily_stake': daily_stake,
342
                'rel_profit': round(curdayprofit / daily_stake, 8) if daily_stake > 0 else 0,
343
                'trades': len(trades),
344
            }
345

346
        data = [
1✔
347
            {
348
                'date': f"{key.year}-{key.month:02d}" if timeunit == 'months' else key,
349
                'abs_profit': value["amount"],
350
                'starting_balance': value["daily_stake"],
351
                'rel_profit': value["rel_profit"],
352
                'fiat_value': self._fiat_converter.convert_amount(
353
                    value['amount'],
354
                    stake_currency,
355
                    fiat_display_currency
356
                ) if self._fiat_converter else 0,
357
                'trade_count': value["trades"],
358
            }
359
            for key, value in profit_units.items()
360
        ]
361
        return {
1✔
362
            'stake_currency': stake_currency,
363
            'fiat_display_currency': fiat_display_currency,
364
            'data': data
365
        }
366

367
    def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict:
1✔
368
        """ Returns the X last trades """
369
        order_by = Trade.id if order_by_id else Trade.close_date.desc()
1✔
370
        if limit:
1✔
371
            trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
1✔
372
                order_by).limit(limit).offset(offset)
373
        else:
374
            trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
1✔
375
                Trade.close_date.desc()).all()
376

377
        output = [trade.to_json() for trade in trades]
1✔
378

379
        return {
1✔
380
            "trades": output,
381
            "trades_count": len(output),
382
            "offset": offset,
383
            "total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(),
384
        }
385

386
    def _rpc_stats(self) -> Dict[str, Any]:
1✔
387
        """
388
        Generate generic stats for trades in database
389
        """
390
        def trade_win_loss(trade):
1✔
391
            if trade.close_profit > 0:
1✔
392
                return 'wins'
1✔
393
            elif trade.close_profit < 0:
1✔
394
                return 'losses'
1✔
395
            else:
396
                return 'draws'
×
397
        trades: List[Trade] = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False)
1✔
398
        # Sell reason
399
        exit_reasons = {}
1✔
400
        for trade in trades:
1✔
401
            if trade.exit_reason not in exit_reasons:
1✔
402
                exit_reasons[trade.exit_reason] = {'wins': 0, 'losses': 0, 'draws': 0}
1✔
403
            exit_reasons[trade.exit_reason][trade_win_loss(trade)] += 1
1✔
404

405
        # Duration
406
        dur: Dict[str, List[int]] = {'wins': [], 'draws': [], 'losses': []}
1✔
407
        for trade in trades:
1✔
408
            if trade.close_date is not None and trade.open_date is not None:
1✔
409
                trade_dur = (trade.close_date - trade.open_date).total_seconds()
1✔
410
                dur[trade_win_loss(trade)].append(trade_dur)
1✔
411

412
        wins_dur = sum(dur['wins']) / len(dur['wins']) if len(dur['wins']) > 0 else None
1✔
413
        draws_dur = sum(dur['draws']) / len(dur['draws']) if len(dur['draws']) > 0 else None
1✔
414
        losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else None
1✔
415

416
        durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur}
1✔
417
        return {'exit_reasons': exit_reasons, 'durations': durations}
1✔
418

419
    def _rpc_trade_statistics(
1✔
420
            self, stake_currency: str, fiat_display_currency: str,
421
            start_date: datetime = datetime.fromtimestamp(0)) -> Dict[str, Any]:
422
        """ Returns cumulative profit statistics """
423
        trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) |
1✔
424
                        Trade.is_open.is_(True))
425
        trades: List[Trade] = Trade.get_trades(
1✔
426
            trade_filter, include_orders=False).order_by(Trade.id).all()
427

428
        profit_all_coin = []
1✔
429
        profit_all_ratio = []
1✔
430
        profit_closed_coin = []
1✔
431
        profit_closed_ratio = []
1✔
432
        durations = []
1✔
433
        winning_trades = 0
1✔
434
        losing_trades = 0
1✔
435
        winning_profit = 0.0
1✔
436
        losing_profit = 0.0
1✔
437

438
        for trade in trades:
1✔
439
            current_rate: float = 0.0
1✔
440

441
            if trade.close_date:
1✔
442
                durations.append((trade.close_date - trade.open_date).total_seconds())
1✔
443

444
            if not trade.is_open:
1✔
445
                profit_ratio = trade.close_profit
1✔
446
                profit_abs = trade.close_profit_abs
1✔
447
                profit_closed_coin.append(profit_abs)
1✔
448
                profit_closed_ratio.append(profit_ratio)
1✔
449
                if trade.close_profit >= 0:
1✔
450
                    winning_trades += 1
1✔
451
                    winning_profit += profit_abs
1✔
452
                else:
453
                    losing_trades += 1
1✔
454
                    losing_profit += profit_abs
1✔
455
            else:
456
                # Get current rate
457
                try:
1✔
458
                    current_rate = self._freqtrade.exchange.get_rate(
1✔
459
                        trade.pair, side='exit', is_short=trade.is_short, refresh=False)
460
                except (PricingError, ExchangeError):
1✔
461
                    current_rate = NAN
1✔
462
                if isnan(current_rate):
1✔
463
                    profit_ratio = NAN
1✔
464
                    profit_abs = NAN
1✔
465
                else:
466
                    profit_ratio = trade.calc_profit_ratio(rate=current_rate)
1✔
467
                    profit_abs = trade.calc_profit(
1✔
468
                        rate=trade.close_rate or current_rate) + trade.realized_profit
469

470
            profit_all_coin.append(profit_abs)
1✔
471
            profit_all_ratio.append(profit_ratio)
1✔
472

473
        best_pair = Trade.get_best_pair(start_date)
1✔
474
        trading_volume = Trade.get_trading_volume(start_date)
1✔
475

476
        # Prepare data to display
477
        profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
1✔
478
        profit_closed_ratio_mean = float(mean(profit_closed_ratio) if profit_closed_ratio else 0.0)
1✔
479
        profit_closed_ratio_sum = sum(profit_closed_ratio) if profit_closed_ratio else 0.0
1✔
480

481
        profit_closed_fiat = self._fiat_converter.convert_amount(
1✔
482
            profit_closed_coin_sum,
483
            stake_currency,
484
            fiat_display_currency
485
        ) if self._fiat_converter else 0
486

487
        profit_all_coin_sum = round(sum(profit_all_coin), 8)
1✔
488
        profit_all_ratio_mean = float(mean(profit_all_ratio) if profit_all_ratio else 0.0)
1✔
489
        # Doing the sum is not right - overall profit needs to be based on initial capital
490
        profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
1✔
491
        starting_balance = self._freqtrade.wallets.get_starting_balance()
1✔
492
        profit_closed_ratio_fromstart = 0
1✔
493
        profit_all_ratio_fromstart = 0
1✔
494
        if starting_balance:
1✔
495
            profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
1✔
496
            profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
1✔
497

498
        profit_factor = winning_profit / abs(losing_profit) if losing_profit else float('inf')
1✔
499

500
        trades_df = DataFrame([{'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT),
1✔
501
                                'profit_abs': trade.close_profit_abs}
502
                               for trade in trades if not trade.is_open])
503
        max_drawdown_abs = 0.0
1✔
504
        max_drawdown = 0.0
1✔
505
        if len(trades_df) > 0:
1✔
506
            try:
1✔
507
                (max_drawdown_abs, _, _, _, _, max_drawdown) = calculate_max_drawdown(
1✔
508
                    trades_df, value_col='profit_abs', starting_balance=starting_balance)
509
            except ValueError:
1✔
510
                # ValueError if no losing trade.
511
                pass
1✔
512

513
        profit_all_fiat = self._fiat_converter.convert_amount(
1✔
514
            profit_all_coin_sum,
515
            stake_currency,
516
            fiat_display_currency
517
        ) if self._fiat_converter else 0
518

519
        first_date = trades[0].open_date if trades else None
1✔
520
        last_date = trades[-1].open_date if trades else None
1✔
521
        num = float(len(durations) or 1)
1✔
522
        return {
1✔
523
            'profit_closed_coin': profit_closed_coin_sum,
524
            'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2),
525
            'profit_closed_ratio_mean': profit_closed_ratio_mean,
526
            'profit_closed_percent_sum': round(profit_closed_ratio_sum * 100, 2),
527
            'profit_closed_ratio_sum': profit_closed_ratio_sum,
528
            'profit_closed_ratio': profit_closed_ratio_fromstart,
529
            'profit_closed_percent': round(profit_closed_ratio_fromstart * 100, 2),
530
            'profit_closed_fiat': profit_closed_fiat,
531
            'profit_all_coin': profit_all_coin_sum,
532
            'profit_all_percent_mean': round(profit_all_ratio_mean * 100, 2),
533
            'profit_all_ratio_mean': profit_all_ratio_mean,
534
            'profit_all_percent_sum': round(profit_all_ratio_sum * 100, 2),
535
            'profit_all_ratio_sum': profit_all_ratio_sum,
536
            'profit_all_ratio': profit_all_ratio_fromstart,
537
            'profit_all_percent': round(profit_all_ratio_fromstart * 100, 2),
538
            'profit_all_fiat': profit_all_fiat,
539
            'trade_count': len(trades),
540
            'closed_trade_count': len([t for t in trades if not t.is_open]),
541
            'first_trade_date': arrow.get(first_date).humanize() if first_date else '',
542
            'first_trade_timestamp': int(first_date.timestamp() * 1000) if first_date else 0,
543
            'latest_trade_date': arrow.get(last_date).humanize() if last_date else '',
544
            'latest_trade_timestamp': int(last_date.timestamp() * 1000) if last_date else 0,
545
            'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
546
            'best_pair': best_pair[0] if best_pair else '',
547
            'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0,  # Deprecated
548
            'best_pair_profit_ratio': best_pair[1] if best_pair else 0,
549
            'winning_trades': winning_trades,
550
            'losing_trades': losing_trades,
551
            'profit_factor': profit_factor,
552
            'max_drawdown': max_drawdown,
553
            'max_drawdown_abs': max_drawdown_abs,
554
            'trading_volume': trading_volume,
555
        }
556

557
    def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
1✔
558
        """ Returns current account balance per crypto """
559
        currencies: List[Dict] = []
1✔
560
        total = 0.0
1✔
561
        try:
1✔
562
            tickers = self._freqtrade.exchange.get_tickers(cached=True)
1✔
563
        except (ExchangeError):
1✔
564
            raise RPCException('Error getting current tickers.')
1✔
565

566
        self._freqtrade.wallets.update(require_update=False)
1✔
567
        starting_capital = self._freqtrade.wallets.get_starting_balance()
1✔
568
        starting_cap_fiat = self._fiat_converter.convert_amount(
1✔
569
            starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0
570
        coin: str
571
        balance: Wallet
572
        for coin, balance in self._freqtrade.wallets.get_all_balances().items():
1✔
573
            if not balance.total:
1✔
574
                continue
1✔
575

576
            est_stake: float = 0
1✔
577
            if coin == stake_currency:
1✔
578
                rate = 1.0
1✔
579
                est_stake = balance.total
1✔
580
                if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
1✔
581
                    # in Futures, "total" includes the locked stake, and therefore all positions
582
                    est_stake = balance.free
1✔
583
            else:
584
                try:
1✔
585
                    pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
1✔
586
                    rate = tickers.get(pair, {}).get('last')
1✔
587
                    if rate:
1✔
588
                        if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
1✔
589
                            rate = 1.0 / rate
1✔
590
                        est_stake = rate * balance.total
1✔
591
                except (ExchangeError):
×
592
                    logger.warning(f" Could not get rate for pair {coin}.")
×
593
                    continue
×
594
            total = total + est_stake
1✔
595
            currencies.append({
1✔
596
                'currency': coin,
597
                'free': balance.free,
598
                'balance': balance.total,
599
                'used': balance.used,
600
                'est_stake': est_stake or 0,
601
                'stake': stake_currency,
602
                'side': 'long',
603
                'leverage': 1,
604
                'position': 0,
605
                'is_position': False,
606
            })
607
        symbol: str
608
        position: PositionWallet
609
        for symbol, position in self._freqtrade.wallets.get_all_positions().items():
1✔
610
            total += position.collateral
1✔
611

612
            currencies.append({
1✔
613
                'currency': symbol,
614
                'free': 0,
615
                'balance': 0,
616
                'used': 0,
617
                'position': position.position,
618
                'est_stake': position.collateral,
619
                'stake': stake_currency,
620
                'leverage': position.leverage,
621
                'side': position.side,
622
                'is_position': True
623
            })
624

625
        value = self._fiat_converter.convert_amount(
1✔
626
            total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
627

628
        trade_count = len(Trade.get_trades_proxy())
1✔
629
        starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0
1✔
630
        starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
1✔
631

632
        return {
1✔
633
            'currencies': currencies,
634
            'total': total,
635
            'symbol': fiat_display_currency,
636
            'value': value,
637
            'stake': stake_currency,
638
            'starting_capital': starting_capital,
639
            'starting_capital_ratio': starting_capital_ratio,
640
            'starting_capital_pct': round(starting_capital_ratio * 100, 2),
641
            'starting_capital_fiat': starting_cap_fiat,
642
            'starting_capital_fiat_ratio': starting_cap_fiat_ratio,
643
            'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2),
644
            'trade_count': trade_count,
645
            'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else ''
646
        }
647

648
    def _rpc_start(self) -> Dict[str, str]:
1✔
649
        """ Handler for start """
650
        if self._freqtrade.state == State.RUNNING:
1✔
651
            return {'status': 'already running'}
1✔
652

653
        self._freqtrade.state = State.RUNNING
1✔
654
        return {'status': 'starting trader ...'}
1✔
655

656
    def _rpc_stop(self) -> Dict[str, str]:
1✔
657
        """ Handler for stop """
658
        if self._freqtrade.state == State.RUNNING:
1✔
659
            self._freqtrade.state = State.STOPPED
1✔
660
            return {'status': 'stopping trader ...'}
1✔
661

662
        return {'status': 'already stopped'}
1✔
663

664
    def _rpc_reload_config(self) -> Dict[str, str]:
1✔
665
        """ Handler for reload_config. """
666
        self._freqtrade.state = State.RELOAD_CONFIG
1✔
667
        return {'status': 'Reloading config ...'}
1✔
668

669
    def _rpc_stopentry(self) -> Dict[str, str]:
1✔
670
        """
671
        Handler to stop buying, but handle open trades gracefully.
672
        """
673
        if self._freqtrade.state == State.RUNNING:
1✔
674
            # Set 'max_open_trades' to 0
675
            self._freqtrade.config['max_open_trades'] = 0
1✔
676

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

679
    def __exec_force_exit(self, trade: Trade, ordertype: Optional[str],
1✔
680
                          amount: Optional[float] = None) -> None:
681
        # Check if there is there is an open order
682
        fully_canceled = False
1✔
683
        if trade.open_order_id:
1✔
684
            order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
1✔
685

686
            if order['side'] == trade.entry_side:
1✔
687
                fully_canceled = self._freqtrade.handle_cancel_enter(
1✔
688
                    trade, order, CANCEL_REASON['FORCE_EXIT'])
689

690
            if order['side'] == trade.exit_side:
1✔
691
                # Cancel order - so it is placed anew with a fresh price.
692
                self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_EXIT'])
1✔
693

694
        if not fully_canceled:
1✔
695
            # Get current rate and execute sell
696
            current_rate = self._freqtrade.exchange.get_rate(
1✔
697
                trade.pair, side='exit', is_short=trade.is_short, refresh=True)
698
            exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT)
1✔
699
            order_type = ordertype or self._freqtrade.strategy.order_types.get(
1✔
700
                "force_exit", self._freqtrade.strategy.order_types["exit"])
701
            sub_amount: Optional[float] = None
1✔
702
            if amount and amount < trade.amount:
1✔
703
                # Partial exit ...
704
                min_exit_stake = self._freqtrade.exchange.get_min_pair_stake_amount(
1✔
705
                    trade.pair, current_rate, trade.stop_loss_pct)
706
                remaining = (trade.amount - amount) * current_rate
1✔
707
                if remaining < min_exit_stake:
1✔
708
                    raise RPCException(f'Remaining amount of {remaining} would be too small.')
×
709
                sub_amount = amount
1✔
710

711
            self._freqtrade.execute_trade_exit(
1✔
712
                trade, current_rate, exit_check, ordertype=order_type,
713
                sub_trade_amt=sub_amount)
714

715
    def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None, *,
1✔
716
                        amount: Optional[float] = None) -> Dict[str, str]:
717
        """
718
        Handler for forceexit <id>.
719
        Sells the given trade at current price
720
        """
721

722
        if self._freqtrade.state != State.RUNNING:
1✔
723
            raise RPCException('trader is not running')
1✔
724

725
        with self._freqtrade._exit_lock:
1✔
726
            if trade_id == 'all':
1✔
727
                # Execute sell for all open orders
728
                for trade in Trade.get_open_trades():
1✔
729
                    self.__exec_force_exit(trade, ordertype)
1✔
730
                Trade.commit()
1✔
731
                self._freqtrade.wallets.update()
1✔
732
                return {'result': 'Created sell orders for all open trades.'}
1✔
733

734
            # Query for trade
735
            trade = Trade.get_trades(
1✔
736
                trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ]
737
            ).first()
738
            if not trade:
1✔
739
                logger.warning('force_exit: Invalid argument received')
1✔
740
                raise RPCException('invalid argument')
1✔
741

742
            self.__exec_force_exit(trade, ordertype, amount)
1✔
743
            Trade.commit()
1✔
744
            self._freqtrade.wallets.update()
1✔
745
            return {'result': f'Created sell order for trade {trade_id}.'}
1✔
746

747
    def _force_entry_validations(self, pair: str, order_side: SignalDirection):
1✔
748
        if not self._freqtrade.config.get('force_entry_enable', False):
1✔
749
            raise RPCException('Force_entry not enabled.')
1✔
750

751
        if self._freqtrade.state != State.RUNNING:
1✔
752
            raise RPCException('trader is not running')
1✔
753

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

757
        if pair not in self._freqtrade.exchange.get_markets(tradable_only=True):
1✔
758
            raise RPCException('Symbol does not exist or market is not active.')
1✔
759
        # Check if pair quote currency equals to the stake currency.
760
        stake_currency = self._freqtrade.config.get('stake_currency')
1✔
761
        if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency:
1✔
762
            raise RPCException(
1✔
763
                f'Wrong pair selected. Only pairs with stake-currency {stake_currency} allowed.')
764

765
    def _rpc_force_entry(self, pair: str, price: Optional[float], *,
1✔
766
                         order_type: Optional[str] = None,
767
                         order_side: SignalDirection = SignalDirection.LONG,
768
                         stake_amount: Optional[float] = None,
769
                         enter_tag: Optional[str] = 'force_entry',
770
                         leverage: Optional[float] = None) -> Optional[Trade]:
771
        """
772
        Handler for forcebuy <asset> <price>
773
        Buys a pair trade at the given or current price
774
        """
775
        self._force_entry_validations(pair, order_side)
1✔
776

777
        # check if valid pair
778

779
        # check if pair already has an open pair
780
        trade: Trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
1✔
781
        is_short = (order_side == SignalDirection.SHORT)
1✔
782
        if trade:
1✔
783
            is_short = trade.is_short
1✔
784
            if not self._freqtrade.strategy.position_adjustment_enable:
1✔
785
                raise RPCException(f'position for {pair} already open - id: {trade.id}')
1✔
786
            if trade.open_order_id is not None:
1✔
787
                raise RPCException(f'position for {pair} already open - id: {trade.id} '
1✔
788
                                   f'and has open order {trade.open_order_id}')
789
        else:
790
            if Trade.get_open_trade_count() >= self._config['max_open_trades']:
1✔
791
                raise RPCException("Maximum number of trades is reached.")
1✔
792

793
        if not stake_amount:
1✔
794
            # gen stake amount
795
            stake_amount = self._freqtrade.wallets.get_trade_stake_amount(pair)
1✔
796

797
        # execute buy
798
        if not order_type:
1✔
799
            order_type = self._freqtrade.strategy.order_types.get(
1✔
800
                'force_entry', self._freqtrade.strategy.order_types['entry'])
801
        with self._freqtrade._exit_lock:
1✔
802
            if self._freqtrade.execute_entry(pair, stake_amount, price,
1✔
803
                                             ordertype=order_type, trade=trade,
804
                                             is_short=is_short,
805
                                             enter_tag=enter_tag,
806
                                             leverage_=leverage,
807
                                             ):
808
                Trade.commit()
1✔
809
                trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
1✔
810
                return trade
1✔
811
            else:
812
                raise RPCException(f'Failed to enter position for {pair}.')
1✔
813

814
    def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]:
1✔
815
        """
816
        Handler for delete <id>.
817
        Delete the given trade and close eventually existing open orders.
818
        """
819
        with self._freqtrade._exit_lock:
1✔
820
            c_count = 0
1✔
821
            trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
1✔
822
            if not trade:
1✔
823
                logger.warning('delete trade: Invalid argument received')
1✔
824
                raise RPCException('invalid argument')
1✔
825

826
            # Try cancelling regular order if that exists
827
            if trade.open_order_id:
1✔
828
                try:
1✔
829
                    self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair)
1✔
830
                    c_count += 1
1✔
831
                except (ExchangeError):
1✔
832
                    pass
1✔
833

834
            # cancel stoploss on exchange ...
835
            if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange')
1✔
836
                    and trade.stoploss_order_id):
837
                try:
1✔
838
                    self._freqtrade.exchange.cancel_stoploss_order(trade.stoploss_order_id,
1✔
839
                                                                   trade.pair)
840
                    c_count += 1
1✔
841
                except (ExchangeError):
1✔
842
                    pass
1✔
843

844
            trade.delete()
1✔
845
            self._freqtrade.wallets.update()
1✔
846
            return {
1✔
847
                'result': 'success',
848
                'trade_id': trade_id,
849
                'result_msg': f'Deleted trade {trade_id}. Closed {c_count} open orders.',
850
                'cancel_order_count': c_count,
851
            }
852

853
    def _rpc_performance(self) -> List[Dict[str, Any]]:
1✔
854
        """
855
        Handler for performance.
856
        Shows a performance statistic from finished trades
857
        """
858
        pair_rates = Trade.get_overall_performance()
1✔
859

860
        return pair_rates
1✔
861

862
    def _rpc_enter_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
863
        """
864
        Handler for buy tag performance.
865
        Shows a performance statistic from finished trades
866
        """
867
        return Trade.get_enter_tag_performance(pair)
1✔
868

869
    def _rpc_exit_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
870
        """
871
        Handler for exit reason performance.
872
        Shows a performance statistic from finished trades
873
        """
874
        return Trade.get_exit_reason_performance(pair)
1✔
875

876
    def _rpc_mix_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
877
        """
878
        Handler for mix tag (enter_tag + exit_reason) performance.
879
        Shows a performance statistic from finished trades
880
        """
881
        mix_tags = Trade.get_mix_tag_performance(pair)
1✔
882

883
        return mix_tags
1✔
884

885
    def _rpc_count(self) -> Dict[str, float]:
1✔
886
        """ Returns the number of trades running """
887
        if self._freqtrade.state != State.RUNNING:
1✔
888
            raise RPCException('trader is not running')
1✔
889

890
        trades = Trade.get_open_trades()
1✔
891
        return {
1✔
892
            'current': len(trades),
893
            'max': (int(self._freqtrade.config['max_open_trades'])
894
                    if self._freqtrade.config['max_open_trades'] != float('inf') else -1),
895
            'total_stake': sum((trade.open_rate * trade.amount) for trade in trades)
896
        }
897

898
    def _rpc_locks(self) -> Dict[str, Any]:
1✔
899
        """ Returns the  current locks """
900

901
        locks = PairLocks.get_pair_locks(None)
1✔
902
        return {
1✔
903
            'lock_count': len(locks),
904
            'locks': [lock.to_json() for lock in locks]
905
        }
906

907
    def _rpc_delete_lock(self, lockid: Optional[int] = None,
1✔
908
                         pair: Optional[str] = None) -> Dict[str, Any]:
909
        """ Delete specific lock(s) """
910
        locks = []
1✔
911

912
        if pair:
1✔
913
            locks = PairLocks.get_pair_locks(pair)
1✔
914
        if lockid:
1✔
915
            locks = PairLock.query.filter(PairLock.id == lockid).all()
1✔
916

917
        for lock in locks:
1✔
918
            lock.active = False
1✔
919
            lock.lock_end_time = datetime.now(timezone.utc)
1✔
920

921
        Trade.commit()
1✔
922

923
        return self._rpc_locks()
1✔
924

925
    def _rpc_whitelist(self) -> Dict:
1✔
926
        """ Returns the currently active whitelist"""
927
        res = {'method': self._freqtrade.pairlists.name_list,
1✔
928
               'length': len(self._freqtrade.active_pair_whitelist),
929
               'whitelist': self._freqtrade.active_pair_whitelist
930
               }
931
        return res
1✔
932

933
    def _rpc_blacklist_delete(self, delete: List[str]) -> Dict:
1✔
934
        """ Removes pairs from currently active blacklist """
935
        errors = {}
1✔
936
        for pair in delete:
1✔
937
            if pair in self._freqtrade.pairlists.blacklist:
1✔
938
                self._freqtrade.pairlists.blacklist.remove(pair)
1✔
939
            else:
940
                errors[pair] = {
1✔
941
                    'error_msg': f"Pair {pair} is not in the current blacklist."
942
                }
943
        resp = self._rpc_blacklist()
1✔
944
        resp['errors'] = errors
1✔
945
        return resp
1✔
946

947
    def _rpc_blacklist(self, add: List[str] = None) -> Dict:
1✔
948
        """ Returns the currently active blacklist"""
949
        errors = {}
1✔
950
        if add:
1✔
951
            for pair in add:
1✔
952
                if pair not in self._freqtrade.pairlists.blacklist:
1✔
953
                    try:
1✔
954
                        expand_pairlist([pair], self._freqtrade.exchange.get_markets().keys())
1✔
955
                        self._freqtrade.pairlists.blacklist.append(pair)
1✔
956

957
                    except ValueError:
1✔
958
                        errors[pair] = {
1✔
959
                            'error_msg': f'Pair {pair} is not a valid wildcard.'}
960
                else:
961
                    errors[pair] = {
1✔
962
                        'error_msg': f'Pair {pair} already in pairlist.'}
963

964
        res = {'method': self._freqtrade.pairlists.name_list,
1✔
965
               'length': len(self._freqtrade.pairlists.blacklist),
966
               'blacklist': self._freqtrade.pairlists.blacklist,
967
               'blacklist_expanded': self._freqtrade.pairlists.expanded_blacklist,
968
               'errors': errors,
969
               }
970
        return res
1✔
971

972
    @staticmethod
1✔
973
    def _rpc_get_logs(limit: Optional[int]) -> Dict[str, Any]:
1✔
974
        """Returns the last X logs"""
975
        if limit:
1✔
976
            buffer = bufferHandler.buffer[-limit:]
1✔
977
        else:
978
            buffer = bufferHandler.buffer
1✔
979
        records = [[datetime.fromtimestamp(r.created).strftime(DATETIME_PRINT_FORMAT),
1✔
980
                   r.created * 1000, r.name, r.levelname,
981
                   r.message + ('\n' + r.exc_text if r.exc_text else '')]
982
                   for r in buffer]
983

984
        # Log format:
985
        # [logtime-formatted, logepoch, logger-name, loglevel, message \n + exception]
986
        # e.g. ["2020-08-27 11:35:01", 1598520901097.9397,
987
        #       "freqtrade.worker", "INFO", "Starting worker develop"]
988

989
        return {'log_count': len(records), 'logs': records}
1✔
990

991
    def _rpc_edge(self) -> List[Dict[str, Any]]:
1✔
992
        """ Returns information related to Edge """
993
        if not self._freqtrade.edge:
1✔
994
            raise RPCException('Edge is not enabled.')
1✔
995
        return self._freqtrade.edge.accepted_pairs()
1✔
996

997
    @staticmethod
1✔
998
    def _convert_dataframe_to_dict(strategy: str, pair: str, timeframe: str, dataframe: DataFrame,
1✔
999
                                   last_analyzed: datetime) -> Dict[str, Any]:
1000
        has_content = len(dataframe) != 0
1✔
1001
        signals = {
1✔
1002
            'enter_long': 0,
1003
            'exit_long': 0,
1004
            'enter_short': 0,
1005
            'exit_short': 0,
1006
        }
1007
        if has_content:
1✔
1008

1009
            dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000
1✔
1010
            # Move signal close to separate column when signal for easy plotting
1011
            for sig_type in signals.keys():
1✔
1012
                if sig_type in dataframe.columns:
1✔
1013
                    mask = (dataframe[sig_type] == 1)
1✔
1014
                    signals[sig_type] = int(mask.sum())
1✔
1015
                    dataframe.loc[mask, f'_{sig_type}_signal_close'] = dataframe.loc[mask, 'close']
1✔
1016

1017
            # band-aid until this is fixed:
1018
            # https://github.com/pandas-dev/pandas/issues/45836
1019
            datetime_types = ['datetime', 'datetime64', 'datetime64[ns, UTC]']
1✔
1020
            date_columns = dataframe.select_dtypes(include=datetime_types)
1✔
1021
            for date_column in date_columns:
1✔
1022
                # replace NaT with `None`
1023
                dataframe[date_column] = dataframe[date_column].astype(object).replace({NaT: None})
1✔
1024

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

1027
        res = {
1✔
1028
            'pair': pair,
1029
            'timeframe': timeframe,
1030
            'timeframe_ms': timeframe_to_msecs(timeframe),
1031
            'strategy': strategy,
1032
            'columns': list(dataframe.columns),
1033
            'data': dataframe.values.tolist(),
1034
            'length': len(dataframe),
1035
            'buy_signals': signals['enter_long'],  # Deprecated
1036
            'sell_signals': signals['exit_long'],  # Deprecated
1037
            'enter_long_signals': signals['enter_long'],
1038
            'exit_long_signals': signals['exit_long'],
1039
            'enter_short_signals': signals['enter_short'],
1040
            'exit_short_signals': signals['exit_short'],
1041
            'last_analyzed': last_analyzed,
1042
            'last_analyzed_ts': int(last_analyzed.timestamp()),
1043
            'data_start': '',
1044
            'data_start_ts': 0,
1045
            'data_stop': '',
1046
            'data_stop_ts': 0,
1047
        }
1048
        if has_content:
1✔
1049
            res.update({
1✔
1050
                'data_start': str(dataframe.iloc[0]['date']),
1051
                'data_start_ts': int(dataframe.iloc[0]['__date_ts']),
1052
                'data_stop': str(dataframe.iloc[-1]['date']),
1053
                'data_stop_ts': int(dataframe.iloc[-1]['__date_ts']),
1054
            })
1055
        return res
1✔
1056

1057
    def _rpc_analysed_dataframe(self, pair: str, timeframe: str,
1✔
1058
                                limit: Optional[int]) -> Dict[str, Any]:
1059
        """ Analyzed dataframe in Dict form """
1060

1061
        _data, last_analyzed = self.__rpc_analysed_dataframe_raw(pair, timeframe, limit)
1✔
1062
        return self._convert_dataframe_to_dict(self._freqtrade.config['strategy'],
1✔
1063
                                               pair, timeframe, _data, last_analyzed)
1064

1065
    def __rpc_analysed_dataframe_raw(
1✔
1066
        self,
1067
        pair: str,
1068
        timeframe: str,
1069
        limit: Optional[int]
1070
    ) -> Tuple[DataFrame, datetime]:
1071
        """
1072
        Get the dataframe and last analyze from the dataprovider
1073

1074
        :param pair: The pair to get
1075
        :param timeframe: The timeframe of data to get
1076
        :param limit: The amount of candles in the dataframe
1077
        """
1078
        _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(
1✔
1079
            pair, timeframe)
1080
        _data = _data.copy()
1✔
1081

1082
        if limit:
1✔
1083
            _data = _data.iloc[-limit:]
1✔
1084

1085
        return _data, last_analyzed
1✔
1086

1087
    def _ws_all_analysed_dataframes(
1✔
1088
        self,
1089
        pairlist: List[str],
1090
        limit: Optional[int]
1091
    ) -> Generator[Dict[str, Any], None, None]:
1092
        """
1093
        Get the analysed dataframes of each pair in the pairlist.
1094
        If specified, only return the most recent `limit` candles for
1095
        each dataframe.
1096

1097
        :param pairlist: A list of pairs to get
1098
        :param limit: If an integer, limits the size of dataframe
1099
                      If a list of string date times, only returns those candles
1100
        :returns: A generator of dictionaries with the key, dataframe, and last analyzed timestamp
1101
        """
1102
        timeframe = self._freqtrade.config['timeframe']
1✔
1103
        candle_type = self._freqtrade.config.get('candle_type_def', CandleType.SPOT)
1✔
1104

1105
        for pair in pairlist:
1✔
1106
            dataframe, last_analyzed = self.__rpc_analysed_dataframe_raw(pair, timeframe, limit)
1✔
1107

1108
            yield {
1✔
1109
                "key": (pair, timeframe, candle_type),
1110
                "df": dataframe,
1111
                "la": last_analyzed
1112
            }
1113

1114
    def _ws_request_analyzed_df(
1✔
1115
        self,
1116
        limit: Optional[int] = None,
1117
        pair: Optional[str] = None
1118
    ):
1119
        """ Historical Analyzed Dataframes for WebSocket """
1120
        pairlist = [pair] if pair else self._freqtrade.active_pair_whitelist
1✔
1121

1122
        return self._ws_all_analysed_dataframes(pairlist, limit)
1✔
1123

1124
    def _ws_request_whitelist(self):
1✔
1125
        """ Whitelist data for WebSocket """
1126
        return self._freqtrade.active_pair_whitelist
1✔
1127

1128
    @staticmethod
1✔
1129
    def _rpc_analysed_history_full(config, pair: str, timeframe: str,
1✔
1130
                                   timerange: str, exchange) -> Dict[str, Any]:
1131
        timerange_parsed = TimeRange.parse_timerange(timerange)
1✔
1132

1133
        _data = load_data(
1✔
1134
            datadir=config.get("datadir"),
1135
            pairs=[pair],
1136
            timeframe=timeframe,
1137
            timerange=timerange_parsed,
1138
            data_format=config.get('dataformat_ohlcv', 'json'),
1139
            candle_type=config.get('candle_type_def', CandleType.SPOT)
1140
        )
1141
        if pair not in _data:
1✔
1142
            raise RPCException(f"No data for {pair}, {timeframe} in {timerange} found.")
1✔
1143
        from freqtrade.data.dataprovider import DataProvider
1✔
1144
        from freqtrade.resolvers.strategy_resolver import StrategyResolver
1✔
1145
        strategy = StrategyResolver.load_strategy(config)
1✔
1146
        strategy.dp = DataProvider(config, exchange=exchange, pairlists=None)
1✔
1147

1148
        df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair})
1✔
1149

1150
        return RPC._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe,
1✔
1151
                                              df_analyzed, arrow.Arrow.utcnow().datetime)
1152

1153
    def _rpc_plot_config(self) -> Dict[str, Any]:
1✔
1154
        if (self._freqtrade.strategy.plot_config and
1✔
1155
                'subplots' not in self._freqtrade.strategy.plot_config):
1156
            self._freqtrade.strategy.plot_config['subplots'] = {}
1✔
1157
        return self._freqtrade.strategy.plot_config
1✔
1158

1159
    @staticmethod
1✔
1160
    def _rpc_sysinfo() -> Dict[str, Any]:
1✔
1161
        return {
1✔
1162
            "cpu_pct": psutil.cpu_percent(interval=1, percpu=True),
1163
            "ram_pct": psutil.virtual_memory().percent
1164
        }
1165

1166
    def _health(self) -> Dict[str, Union[str, int]]:
1✔
1167
        last_p = self._freqtrade.last_process
1✔
1168
        return {
1✔
1169
            'last_process': str(last_p),
1170
            'last_process_loc': last_p.astimezone(tzlocal()).strftime(DATETIME_PRINT_FORMAT),
1171
            'last_process_ts': int(last_p.timestamp()),
1172
        }
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