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

freqtrade / freqtrade / 6181253459

08 Sep 2023 06:04AM UTC coverage: 94.614% (+0.06%) from 94.556%
6181253459

push

github-actions

web-flow
Merge pull request #9159 from stash86/fix-adjust

remove old codes when we only can do partial entries

2 of 2 new or added lines in 1 file covered. (100.0%)

19114 of 20202 relevant lines covered (94.61%)

0.95 hits per line

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

98.37
/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.misc import decimals_per_coin
1✔
29
from freqtrade.persistence import KeyStoreKeys, KeyValueStore, Order, PairLocks, Trade
1✔
30
from freqtrade.persistence.models import PairLock
1✔
31
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
1✔
32
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
1✔
33
from freqtrade.rpc.rpc_types import RPCSendMsg
1✔
34
from freqtrade.util import dt_humanize, dt_now, dt_ts_def, format_date, shorten_date
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['max_open_trades']
125
                                if config['max_open_trades'] != 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: List[int] = []) -> 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
                order: Optional[Order] = None
1✔
175
                current_profit_fiat: Optional[float] = None
1✔
176
                total_profit_fiat: Optional[float] = None
1✔
177
                total_profit_abs = 0.0
1✔
178
                total_profit_ratio: Optional[float] = None
1✔
179
                if trade.open_order_id:
1✔
180
                    order = trade.select_order_by_order_id(trade.open_order_id)
1✔
181
                # calculate profit and send message to user
182
                if trade.is_open:
1✔
183
                    try:
1✔
184
                        current_rate = self._freqtrade.exchange.get_rate(
1✔
185
                            trade.pair, side='exit', is_short=trade.is_short, refresh=False)
186
                    except (ExchangeError, PricingError):
1✔
187
                        current_rate = NAN
1✔
188
                    if len(trade.select_filled_orders(trade.entry_side)) > 0:
1✔
189

190
                        current_profit = current_profit_abs = current_profit_fiat = NAN
1✔
191
                        if not isnan(current_rate):
1✔
192
                            prof = trade.calculate_profit(current_rate)
1✔
193
                            current_profit = prof.profit_ratio
1✔
194
                            current_profit_abs = prof.profit_abs
1✔
195
                            total_profit_abs = prof.total_profit
1✔
196
                            total_profit_ratio = prof.total_profit_ratio
1✔
197
                    else:
198
                        current_profit = current_profit_abs = current_profit_fiat = 0.0
1✔
199

200
                else:
201
                    # Closed trade ...
202
                    current_rate = trade.close_rate
1✔
203
                    current_profit = trade.close_profit or 0.0
1✔
204
                    current_profit_abs = trade.close_profit_abs or 0.0
1✔
205

206
                # Calculate fiat profit
207
                if not isnan(current_profit_abs) and self._fiat_converter:
1✔
208
                    current_profit_fiat = self._fiat_converter.convert_amount(
1✔
209
                        current_profit_abs,
210
                        self._freqtrade.config['stake_currency'],
211
                        self._freqtrade.config['fiat_display_currency']
212
                    )
213
                    total_profit_fiat = self._fiat_converter.convert_amount(
1✔
214
                        total_profit_abs,
215
                        self._freqtrade.config['stake_currency'],
216
                        self._freqtrade.config['fiat_display_currency']
217
                    )
218

219
                # Calculate guaranteed profit (in case of trailing stop)
220
                stop_entry = trade.calculate_profit(trade.stop_loss)
1✔
221

222
                stoploss_entry_dist = stop_entry.profit_abs
1✔
223
                stoploss_entry_dist_ratio = stop_entry.profit_ratio
1✔
224

225
                # calculate distance to stoploss
226
                stoploss_current_dist = trade.stop_loss - current_rate
1✔
227
                stoploss_current_dist_ratio = stoploss_current_dist / current_rate
1✔
228

229
                trade_dict = trade.to_json()
1✔
230
                trade_dict.update(dict(
1✔
231
                    close_profit=trade.close_profit if not trade.is_open else None,
232
                    current_rate=current_rate,
233
                    profit_ratio=current_profit,
234
                    profit_pct=round(current_profit * 100, 2),
235
                    profit_abs=current_profit_abs,
236
                    profit_fiat=current_profit_fiat,
237

238
                    total_profit_abs=total_profit_abs,
239
                    total_profit_fiat=total_profit_fiat,
240
                    total_profit_ratio=total_profit_ratio,
241
                    stoploss_current_dist=stoploss_current_dist,
242
                    stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8),
243
                    stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2),
244
                    stoploss_entry_dist=stoploss_entry_dist,
245
                    stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8),
246
                    open_order=(
247
                        f'({order.order_type} {order.side} rem={order.safe_remaining:.8f})' if
248
                        order else None
249
                    ),
250
                ))
251
                results.append(trade_dict)
1✔
252
            return results
1✔
253

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

294
                detail_trade = [
1✔
295
                    f'{trade.id} {direction_str}',
296
                    trade.pair + ('*' if (open_order
297
                                  and open_order.ft_order_side == trade.entry_side) else '')
298
                    + ('**' if (open_order and
299
                                open_order.ft_order_side == trade.exit_side is not None) else ''),
300
                    shorten_date(dt_humanize(trade.open_date, only_distance=True)),
301
                    profit_str
302
                ]
303
                if self._config.get('position_adjustment_enable', False):
1✔
304
                    max_entry_str = ''
1✔
305
                    if self._config.get('max_entry_position_adjustment', -1) > 0:
1✔
306
                        max_entry_str = f"/{self._config['max_entry_position_adjustment'] + 1}"
1✔
307
                    filled_entries = trade.nr_of_successful_entries
1✔
308
                    detail_trade.append(f"{filled_entries}{max_entry_str}")
1✔
309
                trades_list.append(detail_trade)
1✔
310
            profitcol = "Profit"
1✔
311
            if self._fiat_converter:
1✔
312
                profitcol += " (" + fiat_display_currency + ")"
1✔
313

314
            columns = [
1✔
315
                'ID L/S' if nonspot else 'ID',
316
                'Pair',
317
                'Since',
318
                profitcol]
319
            if self._config.get('position_adjustment_enable', False):
1✔
320
                columns.append('# Entries')
1✔
321
            return trades_list, columns, fiat_profit_sum
1✔
322

323
    def _rpc_timeunit_profit(
1✔
324
            self, timescale: int,
325
            stake_currency: str, fiat_display_currency: str,
326
            timeunit: str = 'days') -> Dict[str, Any]:
327
        """
328
        :param timeunit: Valid entries are 'days', 'weeks', 'months'
329
        """
330
        start_date = datetime.now(timezone.utc).date()
1✔
331
        if timeunit == 'weeks':
1✔
332
            # weekly
333
            start_date = start_date - timedelta(days=start_date.weekday())  # Monday
1✔
334
        if timeunit == 'months':
1✔
335
            start_date = start_date.replace(day=1)
1✔
336

337
        def time_offset(step: int):
1✔
338
            if timeunit == 'months':
1✔
339
                return relativedelta(months=step)
1✔
340
            return timedelta(**{timeunit: step})
1✔
341

342
        if not (isinstance(timescale, int) and timescale > 0):
1✔
343
            raise RPCException('timescale must be an integer greater than 0')
1✔
344

345
        profit_units: Dict[date, Dict] = {}
1✔
346
        daily_stake = self._freqtrade.wallets.get_total_stake_amount()
1✔
347

348
        for day in range(0, timescale):
1✔
349
            profitday = start_date - time_offset(day)
1✔
350
            # Only query for necessary columns for performance reasons.
351
            trades = Trade.session.execute(
1✔
352
                select(Trade.close_profit_abs)
353
                .filter(Trade.is_open.is_(False),
354
                        Trade.close_date >= profitday,
355
                        Trade.close_date < (profitday + time_offset(1)))
356
                .order_by(Trade.close_date)
357
            ).all()
358

359
            curdayprofit = sum(
1✔
360
                trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
361
            # Calculate this periods starting balance
362
            daily_stake = daily_stake - curdayprofit
1✔
363
            profit_units[profitday] = {
1✔
364
                'amount': curdayprofit,
365
                'daily_stake': daily_stake,
366
                'rel_profit': round(curdayprofit / daily_stake, 8) if daily_stake > 0 else 0,
367
                'trades': len(trades),
368
            }
369

370
        data = [
1✔
371
            {
372
                'date': key,
373
                'abs_profit': value["amount"],
374
                'starting_balance': value["daily_stake"],
375
                'rel_profit': value["rel_profit"],
376
                'fiat_value': self._fiat_converter.convert_amount(
377
                    value['amount'],
378
                    stake_currency,
379
                    fiat_display_currency
380
                ) if self._fiat_converter else 0,
381
                'trade_count': value["trades"],
382
            }
383
            for key, value in profit_units.items()
384
        ]
385
        return {
1✔
386
            'stake_currency': stake_currency,
387
            'fiat_display_currency': fiat_display_currency,
388
            'data': data
389
        }
390

391
    def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict:
1✔
392
        """ Returns the X last trades """
393
        order_by: Any = Trade.id if order_by_id else Trade.close_date.desc()
1✔
394
        if limit:
1✔
395
            trades = Trade.session.scalars(
1✔
396
                Trade.get_trades_query([Trade.is_open.is_(False)])
397
                .order_by(order_by)
398
                .limit(limit)
399
                .offset(offset))
400
        else:
401
            trades = Trade.session.scalars(
1✔
402
                Trade.get_trades_query([Trade.is_open.is_(False)])
403
                .order_by(Trade.close_date.desc()))
404

405
        output = [trade.to_json() for trade in trades]
1✔
406
        total_trades = Trade.session.scalar(
1✔
407
            select(func.count(Trade.id)).filter(Trade.is_open.is_(False)))
408

409
        return {
1✔
410
            "trades": output,
411
            "trades_count": len(output),
412
            "offset": offset,
413
            "total_trades": total_trades,
414
        }
415

416
    def _rpc_stats(self) -> Dict[str, Any]:
1✔
417
        """
418
        Generate generic stats for trades in database
419
        """
420
        def trade_win_loss(trade):
1✔
421
            if trade.close_profit > 0:
1✔
422
                return 'wins'
1✔
423
            elif trade.close_profit < 0:
1✔
424
                return 'losses'
1✔
425
            else:
426
                return 'draws'
×
427
        trades = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False)
1✔
428
        # Duration
429
        dur: Dict[str, List[float]] = {'wins': [], 'draws': [], 'losses': []}
1✔
430
        # Exit reason
431
        exit_reasons = {}
1✔
432
        for trade in trades:
1✔
433
            if trade.exit_reason not in exit_reasons:
1✔
434
                exit_reasons[trade.exit_reason] = {'wins': 0, 'losses': 0, 'draws': 0}
1✔
435
            exit_reasons[trade.exit_reason][trade_win_loss(trade)] += 1
1✔
436

437
            if trade.close_date is not None and trade.open_date is not None:
1✔
438
                trade_dur = (trade.close_date - trade.open_date).total_seconds()
1✔
439
                dur[trade_win_loss(trade)].append(trade_dur)
1✔
440

441
        wins_dur = sum(dur['wins']) / len(dur['wins']) if len(dur['wins']) > 0 else None
1✔
442
        draws_dur = sum(dur['draws']) / len(dur['draws']) if len(dur['draws']) > 0 else None
1✔
443
        losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else None
1✔
444

445
        durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur}
1✔
446
        return {'exit_reasons': exit_reasons, 'durations': durations}
1✔
447

448
    def _rpc_trade_statistics(
1✔
449
            self, stake_currency: str, fiat_display_currency: str,
450
            start_date: datetime = datetime.fromtimestamp(0)) -> Dict[str, Any]:
451
        """ Returns cumulative profit statistics """
452
        trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) |
1✔
453
                        Trade.is_open.is_(True))
454
        trades: Sequence[Trade] = Trade.session.scalars(Trade.get_trades_query(
1✔
455
            trade_filter, include_orders=False).order_by(Trade.id)).all()
456

457
        profit_all_coin = []
1✔
458
        profit_all_ratio = []
1✔
459
        profit_closed_coin = []
1✔
460
        profit_closed_ratio = []
1✔
461
        durations = []
1✔
462
        winning_trades = 0
1✔
463
        losing_trades = 0
1✔
464
        winning_profit = 0.0
1✔
465
        losing_profit = 0.0
1✔
466

467
        for trade in trades:
1✔
468
            current_rate: float = 0.0
1✔
469

470
            if trade.close_date:
1✔
471
                durations.append((trade.close_date - trade.open_date).total_seconds())
1✔
472

473
            if not trade.is_open:
1✔
474
                profit_ratio = trade.close_profit or 0.0
1✔
475
                profit_abs = trade.close_profit_abs or 0.0
1✔
476
                profit_closed_coin.append(profit_abs)
1✔
477
                profit_closed_ratio.append(profit_ratio)
1✔
478
                if profit_ratio >= 0:
1✔
479
                    winning_trades += 1
1✔
480
                    winning_profit += profit_abs
1✔
481
                else:
482
                    losing_trades += 1
1✔
483
                    losing_profit += profit_abs
1✔
484
            else:
485
                # Get current rate
486
                try:
1✔
487
                    current_rate = self._freqtrade.exchange.get_rate(
1✔
488
                        trade.pair, side='exit', is_short=trade.is_short, refresh=False)
489
                except (PricingError, ExchangeError):
1✔
490
                    current_rate = NAN
1✔
491
                if isnan(current_rate):
1✔
492
                    profit_ratio = NAN
1✔
493
                    profit_abs = NAN
1✔
494
                else:
495
                    profit = trade.calculate_profit(trade.close_rate or current_rate)
1✔
496

497
                    profit_ratio = profit.profit_ratio
1✔
498
                    profit_abs = profit.total_profit
1✔
499

500
            profit_all_coin.append(profit_abs)
1✔
501
            profit_all_ratio.append(profit_ratio)
1✔
502

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

505
        best_pair = Trade.get_best_pair(start_date)
1✔
506
        trading_volume = Trade.get_trading_volume(start_date)
1✔
507

508
        # Prepare data to display
509
        profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
1✔
510
        profit_closed_ratio_mean = float(mean(profit_closed_ratio) if profit_closed_ratio else 0.0)
1✔
511
        profit_closed_ratio_sum = sum(profit_closed_ratio) if profit_closed_ratio else 0.0
1✔
512

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

519
        profit_all_coin_sum = round(sum(profit_all_coin), 8)
1✔
520
        profit_all_ratio_mean = float(mean(profit_all_ratio) if profit_all_ratio else 0.0)
1✔
521
        # Doing the sum is not right - overall profit needs to be based on initial capital
522
        profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
1✔
523
        starting_balance = self._freqtrade.wallets.get_starting_balance()
1✔
524
        profit_closed_ratio_fromstart = 0
1✔
525
        profit_all_ratio_fromstart = 0
1✔
526
        if starting_balance:
1✔
527
            profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
1✔
528
            profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
1✔
529

530
        profit_factor = winning_profit / abs(losing_profit) if losing_profit else float('inf')
1✔
531

532
        winrate = (winning_trades / closed_trade_count) if closed_trade_count > 0 else 0
1✔
533

534
        trades_df = DataFrame([{'close_date': format_date(trade.close_date),
1✔
535
                                'close_date_dt': trade.close_date,
536
                                'profit_abs': trade.close_profit_abs}
537
                               for trade in trades if not trade.is_open and trade.close_date])
538

539
        expectancy, expectancy_ratio = calculate_expectancy(trades_df)
1✔
540

541
        max_drawdown_abs = 0.0
1✔
542
        max_drawdown = 0.0
1✔
543
        drawdown_start: Optional[datetime] = None
1✔
544
        drawdown_end: Optional[datetime] = None
1✔
545
        dd_high_val = dd_low_val = 0.0
1✔
546
        if len(trades_df) > 0:
1✔
547
            try:
1✔
548
                (max_drawdown_abs, drawdown_start, drawdown_end, dd_high_val, dd_low_val,
1✔
549
                 max_drawdown) = calculate_max_drawdown(
550
                    trades_df, value_col='profit_abs', date_col='close_date_dt',
551
                    starting_balance=starting_balance)
552
            except ValueError:
1✔
553
                # ValueError if no losing trade.
554
                pass
1✔
555

556
        profit_all_fiat = self._fiat_converter.convert_amount(
1✔
557
            profit_all_coin_sum,
558
            stake_currency,
559
            fiat_display_currency
560
        ) if self._fiat_converter else 0
561

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

614
    def __balance_get_est_stake(
1✔
615
            self, coin: str, stake_currency: str, amount: float,
616
            balance: Wallet, tickers) -> Tuple[float, float]:
617
        est_stake = 0.0
1✔
618
        est_bot_stake = 0.0
1✔
619
        if coin == stake_currency:
1✔
620
            est_stake = balance.total
1✔
621
            if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
1✔
622
                # in Futures, "total" includes the locked stake, and therefore all positions
623
                est_stake = balance.free
1✔
624
            est_bot_stake = amount
1✔
625
        else:
626
            pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
1✔
627
            rate: Optional[float] = tickers.get(pair, {}).get('last', None)
1✔
628
            if rate:
1✔
629
                if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
1✔
630
                    rate = 1.0 / rate
1✔
631
                est_stake = rate * balance.total
1✔
632
                est_bot_stake = rate * amount
1✔
633

634
        return est_stake, est_bot_stake
1✔
635

636
    def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
1✔
637
        """ Returns current account balance per crypto """
638
        currencies: List[Dict] = []
1✔
639
        total = 0.0
1✔
640
        total_bot = 0.0
1✔
641
        try:
1✔
642
            tickers: Tickers = self._freqtrade.exchange.get_tickers(cached=True)
1✔
643
        except (ExchangeError):
1✔
644
            raise RPCException('Error getting current tickers.')
1✔
645

646
        open_trades: List[Trade] = Trade.get_open_trades()
1✔
647
        open_assets: Dict[str, Trade] = {t.safe_base_currency: t for t in open_trades}
1✔
648
        self._freqtrade.wallets.update(require_update=False)
1✔
649
        starting_capital = self._freqtrade.wallets.get_starting_balance()
1✔
650
        starting_cap_fiat = self._fiat_converter.convert_amount(
1✔
651
            starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0
652
        coin: str
653
        balance: Wallet
654
        for coin, balance in self._freqtrade.wallets.get_all_balances().items():
1✔
655
            if not balance.total:
1✔
656
                continue
1✔
657

658
            trade = open_assets.get(coin, None)
1✔
659
            is_bot_managed = coin == stake_currency or trade is not None
1✔
660
            trade_amount = trade.amount if trade else 0
1✔
661
            if coin == stake_currency:
1✔
662
                trade_amount = self._freqtrade.wallets.get_available_stake_amount()
1✔
663

664
            try:
1✔
665
                est_stake, est_stake_bot = self.__balance_get_est_stake(
1✔
666
                    coin, stake_currency, trade_amount, balance, tickers)
667
            except ValueError:
×
668
                continue
×
669

670
            total += est_stake
1✔
671

672
            if is_bot_managed:
1✔
673
                total_bot += est_stake_bot
1✔
674
            currencies.append({
1✔
675
                'currency': coin,
676
                'free': balance.free,
677
                'balance': balance.total,
678
                'used': balance.used,
679
                'bot_owned': trade_amount,
680
                'est_stake': est_stake or 0,
681
                'est_stake_bot': est_stake_bot if is_bot_managed else 0,
682
                'stake': stake_currency,
683
                'side': 'long',
684
                'leverage': 1,
685
                'position': 0,
686
                'is_bot_managed': is_bot_managed,
687
                'is_position': False,
688
            })
689
        symbol: str
690
        position: PositionWallet
691
        for symbol, position in self._freqtrade.wallets.get_all_positions().items():
1✔
692
            total += position.collateral
1✔
693
            total_bot += position.collateral
1✔
694

695
            currencies.append({
1✔
696
                'currency': symbol,
697
                'free': 0,
698
                'balance': 0,
699
                'used': 0,
700
                'position': position.position,
701
                'est_stake': position.collateral,
702
                'est_stake_bot': position.collateral,
703
                'stake': stake_currency,
704
                'leverage': position.leverage,
705
                'side': position.side,
706
                'is_bot_managed': True,
707
                'is_position': True
708
            })
709

710
        value = self._fiat_converter.convert_amount(
1✔
711
            total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
712
        value_bot = self._fiat_converter.convert_amount(
1✔
713
            total_bot, stake_currency, fiat_display_currency) if self._fiat_converter else 0
714

715
        trade_count = len(Trade.get_trades_proxy())
1✔
716
        starting_capital_ratio = (total_bot / starting_capital) - 1 if starting_capital else 0.0
1✔
717
        starting_cap_fiat_ratio = (value_bot / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
1✔
718

719
        return {
1✔
720
            'currencies': currencies,
721
            'total': total,
722
            'total_bot': total_bot,
723
            'symbol': fiat_display_currency,
724
            'value': value,
725
            'value_bot': value_bot,
726
            'stake': stake_currency,
727
            'starting_capital': starting_capital,
728
            'starting_capital_ratio': starting_capital_ratio,
729
            'starting_capital_pct': round(starting_capital_ratio * 100, 2),
730
            'starting_capital_fiat': starting_cap_fiat,
731
            'starting_capital_fiat_ratio': starting_cap_fiat_ratio,
732
            'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2),
733
            'trade_count': trade_count,
734
            'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else ''
735
        }
736

737
    def _rpc_start(self) -> Dict[str, str]:
1✔
738
        """ Handler for start """
739
        if self._freqtrade.state == State.RUNNING:
1✔
740
            return {'status': 'already running'}
1✔
741

742
        self._freqtrade.state = State.RUNNING
1✔
743
        return {'status': 'starting trader ...'}
1✔
744

745
    def _rpc_stop(self) -> Dict[str, str]:
1✔
746
        """ Handler for stop """
747
        if self._freqtrade.state == State.RUNNING:
1✔
748
            self._freqtrade.state = State.STOPPED
1✔
749
            return {'status': 'stopping trader ...'}
1✔
750

751
        return {'status': 'already stopped'}
1✔
752

753
    def _rpc_reload_config(self) -> Dict[str, str]:
1✔
754
        """ Handler for reload_config. """
755
        self._freqtrade.state = State.RELOAD_CONFIG
1✔
756
        return {'status': 'Reloading config ...'}
1✔
757

758
    def _rpc_stopentry(self) -> Dict[str, str]:
1✔
759
        """
760
        Handler to stop buying, but handle open trades gracefully.
761
        """
762
        if self._freqtrade.state == State.RUNNING:
1✔
763
            # Set 'max_open_trades' to 0
764
            self._freqtrade.config['max_open_trades'] = 0
1✔
765
            self._freqtrade.strategy.max_open_trades = 0
1✔
766

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

769
    def _rpc_reload_trade_from_exchange(self, trade_id: int) -> Dict[str, str]:
1✔
770
        """
771
        Handler for reload_trade_from_exchange.
772
        Reloads a trade from it's orders, should manual interaction have happened.
773
        """
774
        trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
1✔
775
        if not trade:
1✔
776
            raise RPCException(f"Could not find trade with id {trade_id}.")
1✔
777

778
        self._freqtrade.handle_onexchange_order(trade)
1✔
779
        return {'status': 'Reloaded from orders from exchange'}
1✔
780

781
    def __exec_force_exit(self, trade: Trade, ordertype: Optional[str],
1✔
782
                          amount: Optional[float] = None) -> bool:
783
        # Check if there is there is an open order
784
        fully_canceled = False
1✔
785
        if trade.open_order_id:
1✔
786
            order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
1✔
787

788
            if order['side'] == trade.entry_side:
1✔
789
                fully_canceled = self._freqtrade.handle_cancel_enter(
1✔
790
                    trade, order, CANCEL_REASON['FORCE_EXIT'])
791

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

796
        if not fully_canceled:
1✔
797
            if trade.open_order_id is not None:
1✔
798
                # Order cancellation failed, so we can't exit.
799
                return False
×
800
            # Get current rate and execute sell
801
            current_rate = self._freqtrade.exchange.get_rate(
1✔
802
                trade.pair, side='exit', is_short=trade.is_short, refresh=True)
803
            exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT)
1✔
804
            order_type = ordertype or self._freqtrade.strategy.order_types.get(
1✔
805
                "force_exit", self._freqtrade.strategy.order_types["exit"])
806
            sub_amount: Optional[float] = None
1✔
807
            if amount and amount < trade.amount:
1✔
808
                # Partial exit ...
809
                min_exit_stake = self._freqtrade.exchange.get_min_pair_stake_amount(
1✔
810
                    trade.pair, current_rate, trade.stop_loss_pct)
811
                remaining = (trade.amount - amount) * current_rate
1✔
812
                if remaining < min_exit_stake:
1✔
813
                    raise RPCException(f'Remaining amount of {remaining} would be too small.')
×
814
                sub_amount = amount
1✔
815

816
            self._freqtrade.execute_trade_exit(
1✔
817
                trade, current_rate, exit_check, ordertype=order_type,
818
                sub_trade_amt=sub_amount)
819

820
            return True
1✔
821
        return False
×
822

823
    def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None, *,
1✔
824
                        amount: Optional[float] = None) -> Dict[str, str]:
825
        """
826
        Handler for forceexit <id>.
827
        Sells the given trade at current price
828
        """
829

830
        if self._freqtrade.state != State.RUNNING:
1✔
831
            raise RPCException('trader is not running')
1✔
832

833
        with self._freqtrade._exit_lock:
1✔
834
            if trade_id == 'all':
1✔
835
                # Execute exit for all open orders
836
                for trade in Trade.get_open_trades():
1✔
837
                    self.__exec_force_exit(trade, ordertype)
1✔
838
                Trade.commit()
1✔
839
                self._freqtrade.wallets.update()
1✔
840
                return {'result': 'Created exit orders for all open trades.'}
1✔
841

842
            # Query for trade
843
            trade = Trade.get_trades(
1✔
844
                trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ]
845
            ).first()
846
            if not trade:
1✔
847
                logger.warning('force_exit: Invalid argument received')
1✔
848
                raise RPCException('invalid argument')
1✔
849

850
            result = self.__exec_force_exit(trade, ordertype, amount)
1✔
851
            Trade.commit()
1✔
852
            self._freqtrade.wallets.update()
1✔
853
            if not result:
1✔
854
                raise RPCException('Failed to exit trade.')
×
855
            return {'result': f'Created exit order for trade {trade_id}.'}
1✔
856

857
    def _force_entry_validations(self, pair: str, order_side: SignalDirection):
1✔
858
        if not self._freqtrade.config.get('force_entry_enable', False):
1✔
859
            raise RPCException('Force_entry not enabled.')
1✔
860

861
        if self._freqtrade.state != State.RUNNING:
1✔
862
            raise RPCException('trader is not running')
1✔
863

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

867
        if pair not in self._freqtrade.exchange.get_markets(tradable_only=True):
1✔
868
            raise RPCException('Symbol does not exist or market is not active.')
1✔
869
        # Check if pair quote currency equals to the stake currency.
870
        stake_currency = self._freqtrade.config.get('stake_currency')
1✔
871
        if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency:
1✔
872
            raise RPCException(
1✔
873
                f'Wrong pair selected. Only pairs with stake-currency {stake_currency} allowed.')
874

875
    def _rpc_force_entry(self, pair: str, price: Optional[float], *,
1✔
876
                         order_type: Optional[str] = None,
877
                         order_side: SignalDirection = SignalDirection.LONG,
878
                         stake_amount: Optional[float] = None,
879
                         enter_tag: Optional[str] = 'force_entry',
880
                         leverage: Optional[float] = None) -> Optional[Trade]:
881
        """
882
        Handler for forcebuy <asset> <price>
883
        Buys a pair trade at the given or current price
884
        """
885
        self._force_entry_validations(pair, order_side)
1✔
886

887
        # check if valid pair
888

889
        # check if pair already has an open pair
890
        trade: Optional[Trade] = Trade.get_trades(
1✔
891
            [Trade.is_open.is_(True), Trade.pair == pair]).first()
892
        is_short = (order_side == SignalDirection.SHORT)
1✔
893
        if trade:
1✔
894
            is_short = trade.is_short
1✔
895
            if not self._freqtrade.strategy.position_adjustment_enable:
1✔
896
                raise RPCException(f'position for {pair} already open - id: {trade.id}')
1✔
897
            if trade.open_order_id is not None:
1✔
898
                raise RPCException(f'position for {pair} already open - id: {trade.id} '
1✔
899
                                   f'and has open order {trade.open_order_id}')
900
        else:
901
            if Trade.get_open_trade_count() >= self._config['max_open_trades']:
1✔
902
                raise RPCException("Maximum number of trades is reached.")
1✔
903

904
        if not stake_amount:
1✔
905
            # gen stake amount
906
            stake_amount = self._freqtrade.wallets.get_trade_stake_amount(pair)
1✔
907

908
        # execute buy
909
        if not order_type:
1✔
910
            order_type = self._freqtrade.strategy.order_types.get(
1✔
911
                'force_entry', self._freqtrade.strategy.order_types['entry'])
912
        with self._freqtrade._exit_lock:
1✔
913
            if self._freqtrade.execute_entry(pair, stake_amount, price,
1✔
914
                                             ordertype=order_type, trade=trade,
915
                                             is_short=is_short,
916
                                             enter_tag=enter_tag,
917
                                             leverage_=leverage,
918
                                             ):
919
                Trade.commit()
1✔
920
                trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
1✔
921
                return trade
1✔
922
            else:
923
                raise RPCException(f'Failed to enter position for {pair}.')
1✔
924

925
    def _rpc_cancel_open_order(self, trade_id: int):
1✔
926
        if self._freqtrade.state != State.RUNNING:
1✔
927
            raise RPCException('trader is not running')
×
928
        with self._freqtrade._exit_lock:
1✔
929
            # Query for trade
930
            trade = Trade.get_trades(
1✔
931
                trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ]
932
            ).first()
933
            if not trade:
1✔
934
                logger.warning('cancel_open_order: Invalid trade_id received.')
1✔
935
                raise RPCException('Invalid trade_id.')
1✔
936
            if not trade.open_order_id:
1✔
937
                logger.warning('cancel_open_order: No open order for trade_id.')
1✔
938
                raise RPCException('No open order for trade_id.')
1✔
939

940
            try:
1✔
941
                order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
1✔
942
            except ExchangeError as e:
1✔
943
                logger.info(f"Cannot query order for {trade} due to {e}.", exc_info=True)
1✔
944
                raise RPCException("Order not found.")
1✔
945
            self._freqtrade.handle_cancel_order(order, trade, CANCEL_REASON['USER_CANCEL'])
1✔
946
            Trade.commit()
1✔
947

948
    def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]:
1✔
949
        """
950
        Handler for delete <id>.
951
        Delete the given trade and close eventually existing open orders.
952
        """
953
        with self._freqtrade._exit_lock:
1✔
954
            c_count = 0
1✔
955
            trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
1✔
956
            if not trade:
1✔
957
                logger.warning('delete trade: Invalid argument received')
1✔
958
                raise RPCException('invalid argument')
1✔
959

960
            # Try cancelling regular order if that exists
961
            if trade.open_order_id:
1✔
962
                try:
1✔
963
                    self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair)
1✔
964
                    c_count += 1
1✔
965
                except (ExchangeError):
1✔
966
                    pass
1✔
967

968
            # cancel stoploss on exchange ...
969
            if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange')
1✔
970
                    and trade.stoploss_order_id):
971
                try:
1✔
972
                    self._freqtrade.exchange.cancel_stoploss_order(trade.stoploss_order_id,
1✔
973
                                                                   trade.pair)
974
                    c_count += 1
1✔
975
                except (ExchangeError):
1✔
976
                    pass
1✔
977

978
            trade.delete()
1✔
979
            self._freqtrade.wallets.update()
1✔
980
            return {
1✔
981
                'result': 'success',
982
                'trade_id': trade_id,
983
                'result_msg': f'Deleted trade {trade_id}. Closed {c_count} open orders.',
984
                'cancel_order_count': c_count,
985
            }
986

987
    def _rpc_performance(self) -> List[Dict[str, Any]]:
1✔
988
        """
989
        Handler for performance.
990
        Shows a performance statistic from finished trades
991
        """
992
        pair_rates = Trade.get_overall_performance()
1✔
993

994
        return pair_rates
1✔
995

996
    def _rpc_enter_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
997
        """
998
        Handler for buy tag performance.
999
        Shows a performance statistic from finished trades
1000
        """
1001
        return Trade.get_enter_tag_performance(pair)
1✔
1002

1003
    def _rpc_exit_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
1004
        """
1005
        Handler for exit reason performance.
1006
        Shows a performance statistic from finished trades
1007
        """
1008
        return Trade.get_exit_reason_performance(pair)
1✔
1009

1010
    def _rpc_mix_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
1011
        """
1012
        Handler for mix tag (enter_tag + exit_reason) performance.
1013
        Shows a performance statistic from finished trades
1014
        """
1015
        mix_tags = Trade.get_mix_tag_performance(pair)
1✔
1016

1017
        return mix_tags
1✔
1018

1019
    def _rpc_count(self) -> Dict[str, float]:
1✔
1020
        """ Returns the number of trades running """
1021
        if self._freqtrade.state != State.RUNNING:
1✔
1022
            raise RPCException('trader is not running')
1✔
1023

1024
        trades = Trade.get_open_trades()
1✔
1025
        return {
1✔
1026
            'current': len(trades),
1027
            'max': (int(self._freqtrade.config['max_open_trades'])
1028
                    if self._freqtrade.config['max_open_trades'] != float('inf') else -1),
1029
            'total_stake': sum((trade.open_rate * trade.amount) for trade in trades)
1030
        }
1031

1032
    def _rpc_locks(self) -> Dict[str, Any]:
1✔
1033
        """ Returns the  current locks """
1034

1035
        locks = PairLocks.get_pair_locks(None)
1✔
1036
        return {
1✔
1037
            'lock_count': len(locks),
1038
            'locks': [lock.to_json() for lock in locks]
1039
        }
1040

1041
    def _rpc_delete_lock(self, lockid: Optional[int] = None,
1✔
1042
                         pair: Optional[str] = None) -> Dict[str, Any]:
1043
        """ Delete specific lock(s) """
1044
        locks: Sequence[PairLock] = []
1✔
1045

1046
        if pair:
1✔
1047
            locks = PairLocks.get_pair_locks(pair)
1✔
1048
        if lockid:
1✔
1049
            locks = PairLock.session.scalars(select(PairLock).filter(PairLock.id == lockid)).all()
1✔
1050

1051
        for lock in locks:
1✔
1052
            lock.active = False
1✔
1053
            lock.lock_end_time = datetime.now(timezone.utc)
1✔
1054

1055
        Trade.commit()
1✔
1056

1057
        return self._rpc_locks()
1✔
1058

1059
    def _rpc_whitelist(self) -> Dict:
1✔
1060
        """ Returns the currently active whitelist"""
1061
        res = {'method': self._freqtrade.pairlists.name_list,
1✔
1062
               'length': len(self._freqtrade.active_pair_whitelist),
1063
               'whitelist': self._freqtrade.active_pair_whitelist
1064
               }
1065
        return res
1✔
1066

1067
    def _rpc_blacklist_delete(self, delete: List[str]) -> Dict:
1✔
1068
        """ Removes pairs from currently active blacklist """
1069
        errors = {}
1✔
1070
        for pair in delete:
1✔
1071
            if pair in self._freqtrade.pairlists.blacklist:
1✔
1072
                self._freqtrade.pairlists.blacklist.remove(pair)
1✔
1073
            else:
1074
                errors[pair] = {
1✔
1075
                    'error_msg': f"Pair {pair} is not in the current blacklist."
1076
                }
1077
        resp = self._rpc_blacklist()
1✔
1078
        resp['errors'] = errors
1✔
1079
        return resp
1✔
1080

1081
    def _rpc_blacklist(self, add: Optional[List[str]] = None) -> Dict:
1✔
1082
        """ Returns the currently active blacklist"""
1083
        errors = {}
1✔
1084
        if add:
1✔
1085
            for pair in add:
1✔
1086
                if pair not in self._freqtrade.pairlists.blacklist:
1✔
1087
                    try:
1✔
1088
                        expand_pairlist([pair], self._freqtrade.exchange.get_markets().keys())
1✔
1089
                        self._freqtrade.pairlists.blacklist.append(pair)
1✔
1090

1091
                    except ValueError:
1✔
1092
                        errors[pair] = {
1✔
1093
                            'error_msg': f'Pair {pair} is not a valid wildcard.'}
1094
                else:
1095
                    errors[pair] = {
1✔
1096
                        'error_msg': f'Pair {pair} already in pairlist.'}
1097

1098
        res = {'method': self._freqtrade.pairlists.name_list,
1✔
1099
               'length': len(self._freqtrade.pairlists.blacklist),
1100
               'blacklist': self._freqtrade.pairlists.blacklist,
1101
               'blacklist_expanded': self._freqtrade.pairlists.expanded_blacklist,
1102
               'errors': errors,
1103
               }
1104
        return res
1✔
1105

1106
    @staticmethod
1✔
1107
    def _rpc_get_logs(limit: Optional[int]) -> Dict[str, Any]:
1✔
1108
        """Returns the last X logs"""
1109
        if limit:
1✔
1110
            buffer = bufferHandler.buffer[-limit:]
1✔
1111
        else:
1112
            buffer = bufferHandler.buffer
1✔
1113
        records = [[format_date(datetime.fromtimestamp(r.created)),
1✔
1114
                   r.created * 1000, r.name, r.levelname,
1115
                   r.message + ('\n' + r.exc_text if r.exc_text else '')]
1116
                   for r in buffer]
1117

1118
        # Log format:
1119
        # [logtime-formatted, logepoch, logger-name, loglevel, message \n + exception]
1120
        # e.g. ["2020-08-27 11:35:01", 1598520901097.9397,
1121
        #       "freqtrade.worker", "INFO", "Starting worker develop"]
1122

1123
        return {'log_count': len(records), 'logs': records}
1✔
1124

1125
    def _rpc_edge(self) -> List[Dict[str, Any]]:
1✔
1126
        """ Returns information related to Edge """
1127
        if not self._freqtrade.edge:
1✔
1128
            raise RPCException('Edge is not enabled.')
1✔
1129
        return self._freqtrade.edge.accepted_pairs()
1✔
1130

1131
    @staticmethod
1✔
1132
    def _convert_dataframe_to_dict(strategy: str, pair: str, timeframe: str, dataframe: DataFrame,
1✔
1133
                                   last_analyzed: datetime) -> Dict[str, Any]:
1134
        has_content = len(dataframe) != 0
1✔
1135
        signals = {
1✔
1136
            'enter_long': 0,
1137
            'exit_long': 0,
1138
            'enter_short': 0,
1139
            'exit_short': 0,
1140
        }
1141
        if has_content:
1✔
1142

1143
            dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000
1✔
1144
            # Move signal close to separate column when signal for easy plotting
1145
            for sig_type in signals.keys():
1✔
1146
                if sig_type in dataframe.columns:
1✔
1147
                    mask = (dataframe[sig_type] == 1)
1✔
1148
                    signals[sig_type] = int(mask.sum())
1✔
1149
                    dataframe.loc[mask, f'_{sig_type}_signal_close'] = dataframe.loc[mask, 'close']
1✔
1150

1151
            # band-aid until this is fixed:
1152
            # https://github.com/pandas-dev/pandas/issues/45836
1153
            datetime_types = ['datetime', 'datetime64', 'datetime64[ns, UTC]']
1✔
1154
            date_columns = dataframe.select_dtypes(include=datetime_types)
1✔
1155
            for date_column in date_columns:
1✔
1156
                # replace NaT with `None`
1157
                dataframe[date_column] = dataframe[date_column].astype(object).replace({NaT: None})
1✔
1158

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

1161
        res = {
1✔
1162
            'pair': pair,
1163
            'timeframe': timeframe,
1164
            'timeframe_ms': timeframe_to_msecs(timeframe),
1165
            'strategy': strategy,
1166
            'columns': list(dataframe.columns),
1167
            'data': dataframe.values.tolist(),
1168
            'length': len(dataframe),
1169
            'buy_signals': signals['enter_long'],  # Deprecated
1170
            'sell_signals': signals['exit_long'],  # Deprecated
1171
            'enter_long_signals': signals['enter_long'],
1172
            'exit_long_signals': signals['exit_long'],
1173
            'enter_short_signals': signals['enter_short'],
1174
            'exit_short_signals': signals['exit_short'],
1175
            'last_analyzed': last_analyzed,
1176
            'last_analyzed_ts': int(last_analyzed.timestamp()),
1177
            'data_start': '',
1178
            'data_start_ts': 0,
1179
            'data_stop': '',
1180
            'data_stop_ts': 0,
1181
        }
1182
        if has_content:
1✔
1183
            res.update({
1✔
1184
                'data_start': str(dataframe.iloc[0]['date']),
1185
                'data_start_ts': int(dataframe.iloc[0]['__date_ts']),
1186
                'data_stop': str(dataframe.iloc[-1]['date']),
1187
                'data_stop_ts': int(dataframe.iloc[-1]['__date_ts']),
1188
            })
1189
        return res
1✔
1190

1191
    def _rpc_analysed_dataframe(self, pair: str, timeframe: str,
1✔
1192
                                limit: Optional[int]) -> Dict[str, Any]:
1193
        """ Analyzed dataframe in Dict form """
1194

1195
        _data, last_analyzed = self.__rpc_analysed_dataframe_raw(pair, timeframe, limit)
1✔
1196
        return RPC._convert_dataframe_to_dict(self._freqtrade.config['strategy'],
1✔
1197
                                              pair, timeframe, _data, last_analyzed)
1198

1199
    def __rpc_analysed_dataframe_raw(
1✔
1200
        self,
1201
        pair: str,
1202
        timeframe: str,
1203
        limit: Optional[int]
1204
    ) -> Tuple[DataFrame, datetime]:
1205
        """
1206
        Get the dataframe and last analyze from the dataprovider
1207

1208
        :param pair: The pair to get
1209
        :param timeframe: The timeframe of data to get
1210
        :param limit: The amount of candles in the dataframe
1211
        """
1212
        _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(
1✔
1213
            pair, timeframe)
1214
        _data = _data.copy()
1✔
1215

1216
        if limit:
1✔
1217
            _data = _data.iloc[-limit:]
1✔
1218

1219
        return _data, last_analyzed
1✔
1220

1221
    def _ws_all_analysed_dataframes(
1✔
1222
        self,
1223
        pairlist: List[str],
1224
        limit: Optional[int]
1225
    ) -> Generator[Dict[str, Any], None, None]:
1226
        """
1227
        Get the analysed dataframes of each pair in the pairlist.
1228
        If specified, only return the most recent `limit` candles for
1229
        each dataframe.
1230

1231
        :param pairlist: A list of pairs to get
1232
        :param limit: If an integer, limits the size of dataframe
1233
                      If a list of string date times, only returns those candles
1234
        :returns: A generator of dictionaries with the key, dataframe, and last analyzed timestamp
1235
        """
1236
        timeframe = self._freqtrade.config['timeframe']
1✔
1237
        candle_type = self._freqtrade.config.get('candle_type_def', CandleType.SPOT)
1✔
1238

1239
        for pair in pairlist:
1✔
1240
            dataframe, last_analyzed = self.__rpc_analysed_dataframe_raw(pair, timeframe, limit)
1✔
1241

1242
            yield {
1✔
1243
                "key": (pair, timeframe, candle_type),
1244
                "df": dataframe,
1245
                "la": last_analyzed
1246
            }
1247

1248
    def _ws_request_analyzed_df(
1✔
1249
        self,
1250
        limit: Optional[int] = None,
1251
        pair: Optional[str] = None
1252
    ):
1253
        """ Historical Analyzed Dataframes for WebSocket """
1254
        pairlist = [pair] if pair else self._freqtrade.active_pair_whitelist
1✔
1255

1256
        return self._ws_all_analysed_dataframes(pairlist, limit)
1✔
1257

1258
    def _ws_request_whitelist(self):
1✔
1259
        """ Whitelist data for WebSocket """
1260
        return self._freqtrade.active_pair_whitelist
1✔
1261

1262
    @staticmethod
1✔
1263
    def _rpc_analysed_history_full(config: Config, pair: str, timeframe: str,
1✔
1264
                                   exchange) -> Dict[str, Any]:
1265
        timerange_parsed = TimeRange.parse_timerange(config.get('timerange'))
1✔
1266

1267
        from freqtrade.data.converter import trim_dataframe
1✔
1268
        from freqtrade.data.dataprovider import DataProvider
1✔
1269
        from freqtrade.resolvers.strategy_resolver import StrategyResolver
1✔
1270

1271
        strategy = StrategyResolver.load_strategy(config)
1✔
1272
        startup_candles = strategy.startup_candle_count
1✔
1273

1274
        _data = load_data(
1✔
1275
            datadir=config["datadir"],
1276
            pairs=[pair],
1277
            timeframe=timeframe,
1278
            timerange=timerange_parsed,
1279
            data_format=config['dataformat_ohlcv'],
1280
            candle_type=config.get('candle_type_def', CandleType.SPOT),
1281
            startup_candles=startup_candles,
1282
        )
1283
        if pair not in _data:
1✔
1284
            raise RPCException(
1✔
1285
                f"No data for {pair}, {timeframe} in {config.get('timerange')} found.")
1286

1287
        strategy.dp = DataProvider(config, exchange=exchange, pairlists=None)
1✔
1288
        strategy.ft_bot_start()
1✔
1289

1290
        df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair})
1✔
1291
        df_analyzed = trim_dataframe(df_analyzed, timerange_parsed, startup_candles=startup_candles)
1✔
1292

1293
        return RPC._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe,
1✔
1294
                                              df_analyzed.copy(), dt_now())
1295

1296
    def _rpc_plot_config(self) -> Dict[str, Any]:
1✔
1297
        if (self._freqtrade.strategy.plot_config and
1✔
1298
                'subplots' not in self._freqtrade.strategy.plot_config):
1299
            self._freqtrade.strategy.plot_config['subplots'] = {}
1✔
1300
        return self._freqtrade.strategy.plot_config
1✔
1301

1302
    @staticmethod
1✔
1303
    def _rpc_plot_config_with_strategy(config: Config) -> Dict[str, Any]:
1✔
1304

1305
        from freqtrade.resolvers.strategy_resolver import StrategyResolver
1✔
1306
        strategy = StrategyResolver.load_strategy(config)
1✔
1307

1308
        if (strategy.plot_config and 'subplots' not in strategy.plot_config):
1✔
1309
            strategy.plot_config['subplots'] = {}
1✔
1310
        return strategy.plot_config
1✔
1311

1312
    @staticmethod
1✔
1313
    def _rpc_sysinfo() -> Dict[str, Any]:
1✔
1314
        return {
1✔
1315
            "cpu_pct": psutil.cpu_percent(interval=1, percpu=True),
1316
            "ram_pct": psutil.virtual_memory().percent
1317
        }
1318

1319
    def health(self) -> Dict[str, Optional[Union[str, int]]]:
1✔
1320
        last_p = self._freqtrade.last_process
1✔
1321
        if last_p is None:
1✔
1322
            return {
1✔
1323
                "last_process": None,
1324
                "last_process_loc": None,
1325
                "last_process_ts": None,
1326
            }
1327

1328
        return {
×
1329
            "last_process": str(last_p),
1330
            "last_process_loc": format_date(last_p.astimezone(tzlocal())),
1331
            "last_process_ts": int(last_p.timestamp()),
1332
        }
1333

1334
    def _update_market_direction(self, direction: MarketDirection) -> None:
1✔
1335
        self._freqtrade.strategy.market_direction = direction
1✔
1336

1337
    def _get_market_direction(self) -> MarketDirection:
1✔
1338
        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