• 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.03
/freqtrade/persistence/trade_model.py
1
"""
2
This module contains the class to persist trades into SQLite
3
"""
4
import logging
1✔
5
from collections import defaultdict
1✔
6
from dataclasses import dataclass
1✔
7
from datetime import datetime, timedelta, timezone
1✔
8
from math import isclose
1✔
9
from typing import Any, ClassVar, Dict, List, Optional, Sequence, cast
1✔
10

11
from sqlalchemy import (Enum, Float, ForeignKey, Integer, ScalarResult, Select, String,
1✔
12
                        UniqueConstraint, desc, func, select)
13
from sqlalchemy.orm import Mapped, lazyload, mapped_column, relationship, validates
1✔
14
from typing_extensions import Self
1✔
15

16
from freqtrade.constants import (CUSTOM_TAG_MAX_LENGTH, DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC,
1✔
17
                                 NON_OPEN_EXCHANGE_STATES, BuySell, LongShort)
18
from freqtrade.enums import ExitType, TradingMode
1✔
19
from freqtrade.exceptions import DependencyException, OperationalException
1✔
20
from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision,
1✔
21
                                price_to_precision)
22
from freqtrade.leverage import interest
1✔
23
from freqtrade.persistence.base import ModelBase, SessionType
1✔
24
from freqtrade.util import FtPrecise, dt_now
1✔
25

26

27
logger = logging.getLogger(__name__)
1✔
28

29

30
@dataclass
1✔
31
class ProfitStruct:
1✔
32
    profit_abs: float
1✔
33
    profit_ratio: float
1✔
34
    total_profit: float
1✔
35
    total_profit_ratio: float
1✔
36

37

38
class Order(ModelBase):
1✔
39
    """
40
    Order database model
41
    Keeps a record of all orders placed on the exchange
42

43
    One to many relationship with Trades:
44
      - One trade can have many orders
45
      - One Order can only be associated with one Trade
46

47
    Mirrors CCXT Order structure
48
    """
49
    __tablename__ = 'orders'
1✔
50
    __allow_unmapped__ = True
1✔
51
    session: ClassVar[SessionType]
1✔
52

53
    # Uniqueness should be ensured over pair, order_id
54
    # its likely that order_id is unique per Pair on some exchanges.
55
    __table_args__ = (UniqueConstraint('ft_pair', 'order_id', name="_order_pair_order_id"),)
1✔
56

57
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
1✔
58
    ft_trade_id: Mapped[int] = mapped_column(Integer, ForeignKey('trades.id'), index=True)
1✔
59

60
    _trade_live: Mapped["Trade"] = relationship("Trade", back_populates="orders", lazy="immediate")
1✔
61
    _trade_bt: "LocalTrade" = None  # type: ignore
1✔
62

63
    # order_side can only be 'buy', 'sell' or 'stoploss'
64
    ft_order_side: Mapped[str] = mapped_column(String(25), nullable=False)
1✔
65
    ft_pair: Mapped[str] = mapped_column(String(25), nullable=False)
1✔
66
    ft_is_open: Mapped[bool] = mapped_column(nullable=False, default=True, index=True)
1✔
67
    ft_amount: Mapped[float] = mapped_column(Float(), nullable=False)
1✔
68
    ft_price: Mapped[float] = mapped_column(Float(), nullable=False)
1✔
69

70
    order_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
1✔
71
    status: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
1✔
72
    symbol: Mapped[Optional[str]] = mapped_column(String(25), nullable=True)
1✔
73
    # TODO: type: order_type type is Optional[str]
74
    order_type: Mapped[str] = mapped_column(String(50), nullable=True)
1✔
75
    side: Mapped[str] = mapped_column(String(25), nullable=True)
1✔
76
    price: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
1✔
77
    average: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
1✔
78
    amount: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
1✔
79
    filled: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
1✔
80
    remaining: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
1✔
81
    cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
1✔
82
    stop_price: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
1✔
83
    order_date: Mapped[datetime] = mapped_column(nullable=True, default=dt_now)
1✔
84
    order_filled_date: Mapped[Optional[datetime]] = mapped_column(nullable=True)
1✔
85
    order_update_date: Mapped[Optional[datetime]] = mapped_column(nullable=True)
1✔
86
    funding_fee: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
1✔
87

88
    ft_fee_base: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
1✔
89

90
    @property
1✔
91
    def order_date_utc(self) -> datetime:
1✔
92
        """ Order-date with UTC timezoneinfo"""
93
        return self.order_date.replace(tzinfo=timezone.utc)
1✔
94

95
    @property
1✔
96
    def order_filled_utc(self) -> Optional[datetime]:
1✔
97
        """ last order-date with UTC timezoneinfo"""
98
        return (
1✔
99
            self.order_filled_date.replace(tzinfo=timezone.utc) if self.order_filled_date else None
100
        )
101

102
    @property
1✔
103
    def safe_amount(self) -> float:
1✔
104
        return self.amount or self.ft_amount
1✔
105

106
    @property
1✔
107
    def safe_price(self) -> float:
1✔
108
        return self.average or self.price or self.stop_price or self.ft_price
1✔
109

110
    @property
1✔
111
    def safe_filled(self) -> float:
1✔
112
        return self.filled if self.filled is not None else 0.0
1✔
113

114
    @property
1✔
115
    def safe_cost(self) -> float:
1✔
116
        return self.cost or 0.0
×
117

118
    @property
1✔
119
    def safe_remaining(self) -> float:
1✔
120
        return (
1✔
121
            self.remaining if self.remaining is not None else
122
            self.safe_amount - (self.filled or 0.0)
123
        )
124

125
    @property
1✔
126
    def safe_fee_base(self) -> float:
1✔
127
        return self.ft_fee_base or 0.0
1✔
128

129
    @property
1✔
130
    def safe_amount_after_fee(self) -> float:
1✔
131
        return self.safe_filled - self.safe_fee_base
1✔
132

133
    @property
1✔
134
    def trade(self) -> "LocalTrade":
1✔
135
        return self._trade_bt or self._trade_live
1✔
136

137
    @property
1✔
138
    def stake_amount(self) -> float:
1✔
139
        """ Amount in stake currency used for this order"""
140
        return self.safe_amount * self.safe_price / self.trade.leverage
1✔
141

142
    def __repr__(self):
1✔
143

144
        return (f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, "
1✔
145
                f"side={self.side}, filled={self.safe_filled}, price={self.safe_price}, "
146
                f"status={self.status}, date={self.order_date:{DATETIME_PRINT_FORMAT}})")
147

148
    def update_from_ccxt_object(self, order):
1✔
149
        """
150
        Update Order from ccxt response
151
        Only updates if fields are available from ccxt -
152
        """
153
        if self.order_id != str(order['id']):
1✔
154
            raise DependencyException("Order-id's don't match")
1✔
155

156
        self.status = order.get('status', self.status)
1✔
157
        self.symbol = order.get('symbol', self.symbol)
1✔
158
        self.order_type = order.get('type', self.order_type)
1✔
159
        self.side = order.get('side', self.side)
1✔
160
        self.price = order.get('price', self.price)
1✔
161
        self.amount = order.get('amount', self.amount)
1✔
162
        self.filled = order.get('filled', self.filled)
1✔
163
        self.average = order.get('average', self.average)
1✔
164
        self.remaining = order.get('remaining', self.remaining)
1✔
165
        self.cost = order.get('cost', self.cost)
1✔
166
        self.stop_price = order.get('stopPrice', self.stop_price)
1✔
167

168
        if 'timestamp' in order and order['timestamp'] is not None:
1✔
169
            self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
1✔
170

171
        self.ft_is_open = True
1✔
172
        if self.status in NON_OPEN_EXCHANGE_STATES:
1✔
173
            self.ft_is_open = False
1✔
174
            if self.trade:
1✔
175
                # Assign funding fee up to this point
176
                # (represents the funding fee since the last order)
177
                self.funding_fee = self.trade.funding_fees
1✔
178
            if (order.get('filled', 0.0) or 0.0) > 0 and not self.order_filled_date:
1✔
179
                self.order_filled_date = datetime.now(timezone.utc)
1✔
180
        self.order_update_date = datetime.now(timezone.utc)
1✔
181

182
    def to_ccxt_object(self, stopPriceName: str = 'stopPrice') -> Dict[str, Any]:
1✔
183
        order: Dict[str, Any] = {
1✔
184
            'id': self.order_id,
185
            'symbol': self.ft_pair,
186
            'price': self.price,
187
            'average': self.average,
188
            'amount': self.amount,
189
            'cost': self.cost,
190
            'type': self.order_type,
191
            'side': self.ft_order_side,
192
            'filled': self.filled,
193
            'remaining': self.remaining,
194
            'datetime': self.order_date_utc.strftime('%Y-%m-%dT%H:%M:%S.%f'),
195
            'timestamp': int(self.order_date_utc.timestamp() * 1000),
196
            'status': self.status,
197
            'fee': None,
198
            'info': {},
199
        }
200
        if self.ft_order_side == 'stoploss':
1✔
201
            order.update({
1✔
202
                stopPriceName: self.stop_price,
203
                'ft_order_type': 'stoploss',
204
            })
205

206
        return order
1✔
207

208
    def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]:
1✔
209
        resp = {
1✔
210
            'amount': self.safe_amount,
211
            'safe_price': self.safe_price,
212
            'ft_order_side': self.ft_order_side,
213
            'order_filled_timestamp': int(self.order_filled_date.replace(
214
                tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
215
            'ft_is_entry': self.ft_order_side == entry_side,
216
        }
217
        if not minified:
1✔
218
            resp.update({
1✔
219
                'pair': self.ft_pair,
220
                'order_id': self.order_id,
221
                'status': self.status,
222
                'average': round(self.average, 8) if self.average else 0,
223
                'cost': self.cost if self.cost else 0,
224
                'filled': self.filled,
225
                'is_open': self.ft_is_open,
226
                'order_date': self.order_date.strftime(DATETIME_PRINT_FORMAT)
227
                if self.order_date else None,
228
                'order_timestamp': int(self.order_date.replace(
229
                    tzinfo=timezone.utc).timestamp() * 1000) if self.order_date else None,
230
                'order_filled_date': self.order_filled_date.strftime(DATETIME_PRINT_FORMAT)
231
                if self.order_filled_date else None,
232
                'order_type': self.order_type,
233
                'price': self.price,
234
                'remaining': self.remaining,
235
                'ft_fee_base': self.ft_fee_base,
236
            })
237
        return resp
1✔
238

239
    def close_bt_order(self, close_date: datetime, trade: 'LocalTrade'):
1✔
240
        self.order_filled_date = close_date
1✔
241
        self.filled = self.amount
1✔
242
        self.remaining = 0
1✔
243
        self.status = 'closed'
1✔
244
        self.ft_is_open = False
1✔
245
        # Assign funding fees to Order.
246
        # Assumes backtesting will use date_last_filled_utc to calculate future funding fees.
247
        self.funding_fee = trade.funding_fees
1✔
248

249
        if (self.ft_order_side == trade.entry_side and self.price):
1✔
250
            trade.open_rate = self.price
1✔
251
            trade.recalc_trade_from_orders()
1✔
252
            if trade.nr_of_successful_entries == 1:
1✔
253
                trade.initial_stop_loss_pct = None
1✔
254
                trade.is_stop_loss_trailing = False
1✔
255
            trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct)
1✔
256

257
    @staticmethod
1✔
258
    def update_orders(orders: List['Order'], order: Dict[str, Any]):
1✔
259
        """
260
        Get all non-closed orders - useful when trying to batch-update orders
261
        """
262
        if not isinstance(order, dict):
1✔
263
            logger.warning(f"{order} is not a valid response object.")
1✔
264
            return
1✔
265

266
        filtered_orders = [o for o in orders if o.order_id == order.get('id')]
1✔
267
        if filtered_orders:
1✔
268
            oobj = filtered_orders[0]
1✔
269
            oobj.update_from_ccxt_object(order)
1✔
270
            Trade.commit()
1✔
271
        else:
272
            logger.warning(f"Did not find order for {order}.")
1✔
273

274
    @classmethod
1✔
275
    def parse_from_ccxt_object(
1✔
276
            cls, order: Dict[str, Any], pair: str, side: str,
277
            amount: Optional[float] = None, price: Optional[float] = None) -> Self:
278
        """
279
        Parse an order from a ccxt object and return a new order Object.
280
        Optional support for overriding amount and price is only used for test simplification.
281
        """
282
        o = cls(
1✔
283
            order_id=str(order['id']),
284
            ft_order_side=side,
285
            ft_pair=pair,
286
            ft_amount=amount if amount else order['amount'],
287
            ft_price=price if price else order['price'],
288
            )
289

290
        o.update_from_ccxt_object(order)
1✔
291
        return o
1✔
292

293
    @staticmethod
1✔
294
    def get_open_orders() -> Sequence['Order']:
1✔
295
        """
296
        Retrieve open orders from the database
297
        :return: List of open orders
298
        """
299
        return Order.session.scalars(select(Order).filter(Order.ft_is_open.is_(True))).all()
1✔
300

301
    @staticmethod
1✔
302
    def order_by_id(order_id: str) -> Optional['Order']:
1✔
303
        """
304
        Retrieve order based on order_id
305
        :return: Order or None
306
        """
307
        return Order.session.scalars(select(Order).filter(Order.order_id == order_id)).first()
1✔
308

309

310
class LocalTrade:
1✔
311
    """
312
    Trade database model.
313
    Used in backtesting - must be aligned to Trade model!
314

315
    """
316
    use_db: bool = False
1✔
317
    # Trades container for backtesting
318
    trades: List['LocalTrade'] = []
1✔
319
    trades_open: List['LocalTrade'] = []
1✔
320
    # Copy of trades_open - but indexed by pair
321
    bt_trades_open_pp: Dict[str, List['LocalTrade']] = defaultdict(list)
1✔
322
    bt_open_open_trade_count: int = 0
1✔
323
    total_profit: float = 0
1✔
324
    realized_profit: float = 0
1✔
325

326
    id: int = 0
1✔
327

328
    orders: List[Order] = []
1✔
329

330
    exchange: str = ''
1✔
331
    pair: str = ''
1✔
332
    base_currency: Optional[str] = ''
1✔
333
    stake_currency: Optional[str] = ''
1✔
334
    is_open: bool = True
1✔
335
    fee_open: float = 0.0
1✔
336
    fee_open_cost: Optional[float] = None
1✔
337
    fee_open_currency: Optional[str] = ''
1✔
338
    fee_close: Optional[float] = 0.0
1✔
339
    fee_close_cost: Optional[float] = None
1✔
340
    fee_close_currency: Optional[str] = ''
1✔
341
    open_rate: float = 0.0
1✔
342
    open_rate_requested: Optional[float] = None
1✔
343
    # open_trade_value - calculated via _calc_open_trade_value
344
    open_trade_value: float = 0.0
1✔
345
    close_rate: Optional[float] = None
1✔
346
    close_rate_requested: Optional[float] = None
1✔
347
    close_profit: Optional[float] = None
1✔
348
    close_profit_abs: Optional[float] = None
1✔
349
    stake_amount: float = 0.0
1✔
350
    max_stake_amount: Optional[float] = 0.0
1✔
351
    amount: float = 0.0
1✔
352
    amount_requested: Optional[float] = None
1✔
353
    open_date: datetime
1✔
354
    close_date: Optional[datetime] = None
1✔
355
    open_order_id: Optional[str] = None
1✔
356
    # absolute value of the stop loss
357
    stop_loss: float = 0.0
1✔
358
    # percentage value of the stop loss
359
    stop_loss_pct: Optional[float] = 0.0
1✔
360
    # absolute value of the initial stop loss
361
    initial_stop_loss: Optional[float] = 0.0
1✔
362
    # percentage value of the initial stop loss
363
    initial_stop_loss_pct: Optional[float] = None
1✔
364
    is_stop_loss_trailing: bool = False
1✔
365
    # stoploss order id which is on exchange
366
    stoploss_order_id: Optional[str] = None
1✔
367
    # last update time of the stoploss order on exchange
368
    stoploss_last_update: Optional[datetime] = None
1✔
369
    # absolute value of the highest reached price
370
    max_rate: Optional[float] = None
1✔
371
    # Lowest price reached
372
    min_rate: Optional[float] = None
1✔
373
    exit_reason: Optional[str] = ''
1✔
374
    exit_order_status: Optional[str] = ''
1✔
375
    strategy: Optional[str] = ''
1✔
376
    enter_tag: Optional[str] = None
1✔
377
    timeframe: Optional[int] = None
1✔
378

379
    trading_mode: TradingMode = TradingMode.SPOT
1✔
380
    amount_precision: Optional[float] = None
1✔
381
    price_precision: Optional[float] = None
1✔
382
    precision_mode: Optional[int] = None
1✔
383
    contract_size: Optional[float] = None
1✔
384

385
    # Leverage trading properties
386
    liquidation_price: Optional[float] = None
1✔
387
    is_short: bool = False
1✔
388
    leverage: float = 1.0
1✔
389

390
    # Margin trading properties
391
    interest_rate: float = 0.0
1✔
392

393
    # Futures properties
394
    funding_fees: Optional[float] = None
1✔
395

396
    @property
1✔
397
    def stoploss_or_liquidation(self) -> float:
1✔
398
        if self.liquidation_price:
1✔
399
            if self.is_short:
1✔
400
                return min(self.stop_loss, self.liquidation_price)
1✔
401
            else:
402
                return max(self.stop_loss, self.liquidation_price)
1✔
403

404
        return self.stop_loss
1✔
405

406
    @property
1✔
407
    def buy_tag(self) -> Optional[str]:
1✔
408
        """
409
        Compatibility between buy_tag (old) and enter_tag (new)
410
        Consider buy_tag deprecated
411
        """
412
        return self.enter_tag
1✔
413

414
    @property
1✔
415
    def has_no_leverage(self) -> bool:
1✔
416
        """Returns true if this is a non-leverage, non-short trade"""
417
        return ((self.leverage == 1.0 or self.leverage is None) and not self.is_short)
1✔
418

419
    @property
1✔
420
    def borrowed(self) -> float:
1✔
421
        """
422
            The amount of currency borrowed from the exchange for leverage trades
423
            If a long trade, the amount is in base currency
424
            If a short trade, the amount is in the other currency being traded
425
        """
426
        if self.has_no_leverage:
1✔
427
            return 0.0
1✔
428
        elif not self.is_short:
1✔
429
            return (self.amount * self.open_rate) * ((self.leverage - 1) / self.leverage)
1✔
430
        else:
431
            return self.amount
1✔
432

433
    @property
1✔
434
    def date_last_filled_utc(self) -> datetime:
1✔
435
        """ Date of the last filled order"""
436
        orders = self.select_filled_orders()
1✔
437
        if not orders:
1✔
438
            return self.open_date_utc
1✔
439
        return max([self.open_date_utc,
1✔
440
                    max(o.order_filled_utc for o in orders if o.order_filled_utc)])
441

442
    @property
1✔
443
    def open_date_utc(self):
1✔
444
        return self.open_date.replace(tzinfo=timezone.utc)
1✔
445

446
    @property
1✔
447
    def stoploss_last_update_utc(self):
1✔
448
        if self.stoploss_last_update:
1✔
449
            return self.stoploss_last_update.replace(tzinfo=timezone.utc)
1✔
450
        return None
×
451

452
    @property
1✔
453
    def close_date_utc(self):
1✔
454
        return self.close_date.replace(tzinfo=timezone.utc) if self.close_date else None
1✔
455

456
    @property
1✔
457
    def entry_side(self) -> str:
1✔
458
        if self.is_short:
1✔
459
            return "sell"
1✔
460
        else:
461
            return "buy"
1✔
462

463
    @property
1✔
464
    def exit_side(self) -> BuySell:
1✔
465
        if self.is_short:
1✔
466
            return "buy"
1✔
467
        else:
468
            return "sell"
1✔
469

470
    @property
1✔
471
    def trade_direction(self) -> LongShort:
1✔
472
        if self.is_short:
1✔
473
            return "short"
1✔
474
        else:
475
            return "long"
1✔
476

477
    @property
1✔
478
    def safe_base_currency(self) -> str:
1✔
479
        """
480
        Compatibility layer for asset - which can be empty for old trades.
481
        """
482
        try:
1✔
483
            return self.base_currency or self.pair.split('/')[0]
1✔
484
        except IndexError:
×
485
            return ''
×
486

487
    @property
1✔
488
    def safe_quote_currency(self) -> str:
1✔
489
        """
490
        Compatibility layer for asset - which can be empty for old trades.
491
        """
492
        try:
1✔
493
            return self.stake_currency or self.pair.split('/')[1].split(':')[0]
1✔
494
        except IndexError:
×
495
            return ''
×
496

497
    def __init__(self, **kwargs):
1✔
498
        for key in kwargs:
1✔
499
            setattr(self, key, kwargs[key])
1✔
500
        self.recalc_open_trade_value()
1✔
501
        if self.trading_mode == TradingMode.MARGIN and self.interest_rate is None:
1✔
502
            raise OperationalException(
×
503
                f"{self.trading_mode.value} trading requires param interest_rate on trades")
504

505
    def __repr__(self):
1✔
506
        open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
1✔
507

508
        return (
1✔
509
            f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
510
            f'is_short={self.is_short or False}, leverage={self.leverage or 1.0}, '
511
            f'open_rate={self.open_rate:.8f}, open_since={open_since})'
512
        )
513

514
    def to_json(self, minified: bool = False) -> Dict[str, Any]:
1✔
515
        filled_orders = self.select_filled_or_open_orders()
1✔
516
        orders = [order.to_json(self.entry_side, minified) for order in filled_orders]
1✔
517

518
        return {
1✔
519
            'trade_id': self.id,
520
            'pair': self.pair,
521
            'base_currency': self.safe_base_currency,
522
            'quote_currency': self.safe_quote_currency,
523
            'is_open': self.is_open,
524
            'exchange': self.exchange,
525
            'amount': round(self.amount, 8),
526
            'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
527
            'stake_amount': round(self.stake_amount, 8),
528
            'max_stake_amount': round(self.max_stake_amount, 8) if self.max_stake_amount else None,
529
            'strategy': self.strategy,
530
            'enter_tag': self.enter_tag,
531
            'timeframe': self.timeframe,
532

533
            'fee_open': self.fee_open,
534
            'fee_open_cost': self.fee_open_cost,
535
            'fee_open_currency': self.fee_open_currency,
536
            'fee_close': self.fee_close,
537
            'fee_close_cost': self.fee_close_cost,
538
            'fee_close_currency': self.fee_close_currency,
539

540
            'open_date': self.open_date.strftime(DATETIME_PRINT_FORMAT),
541
            'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000),
542
            'open_rate': self.open_rate,
543
            'open_rate_requested': self.open_rate_requested,
544
            'open_trade_value': round(self.open_trade_value, 8),
545

546
            'close_date': (self.close_date.strftime(DATETIME_PRINT_FORMAT)
547
                           if self.close_date else None),
548
            'close_timestamp': int(self.close_date.replace(
549
                tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None,
550
            'realized_profit': self.realized_profit or 0.0,
551
            # Close-profit corresponds to relative realized_profit ratio
552
            'realized_profit_ratio': self.close_profit or None,
553
            'close_rate': self.close_rate,
554
            'close_rate_requested': self.close_rate_requested,
555
            'close_profit': self.close_profit,  # Deprecated
556
            'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
557
            'close_profit_abs': self.close_profit_abs,  # Deprecated
558

559
            'trade_duration_s': (int((self.close_date_utc - self.open_date_utc).total_seconds())
560
                                 if self.close_date else None),
561
            'trade_duration': (int((self.close_date_utc - self.open_date_utc).total_seconds() // 60)
562
                               if self.close_date else None),
563

564
            'profit_ratio': self.close_profit,
565
            'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
566
            'profit_abs': self.close_profit_abs,
567

568
            'exit_reason': self.exit_reason,
569
            'exit_order_status': self.exit_order_status,
570
            'stop_loss_abs': self.stop_loss,
571
            'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
572
            'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
573
            'stoploss_order_id': self.stoploss_order_id,
574
            'stoploss_last_update': (self.stoploss_last_update.strftime(DATETIME_PRINT_FORMAT)
575
                                     if self.stoploss_last_update else None),
576
            'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace(
577
                tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None,
578
            'initial_stop_loss_abs': self.initial_stop_loss,
579
            'initial_stop_loss_ratio': (self.initial_stop_loss_pct
580
                                        if self.initial_stop_loss_pct else None),
581
            'initial_stop_loss_pct': (self.initial_stop_loss_pct * 100
582
                                      if self.initial_stop_loss_pct else None),
583
            'min_rate': self.min_rate,
584
            'max_rate': self.max_rate,
585

586
            'leverage': self.leverage,
587
            'interest_rate': self.interest_rate,
588
            'liquidation_price': self.liquidation_price,
589
            'is_short': self.is_short,
590
            'trading_mode': self.trading_mode,
591
            'funding_fees': self.funding_fees,
592
            'open_order_id': self.open_order_id,
593
            'amount_precision': self.amount_precision,
594
            'price_precision': self.price_precision,
595
            'precision_mode': self.precision_mode,
596
            'orders': orders,
597
        }
598

599
    @staticmethod
1✔
600
    def reset_trades() -> None:
1✔
601
        """
602
        Resets all trades. Only active for backtesting mode.
603
        """
604
        LocalTrade.trades = []
1✔
605
        LocalTrade.trades_open = []
1✔
606
        LocalTrade.bt_trades_open_pp = defaultdict(list)
1✔
607
        LocalTrade.bt_open_open_trade_count = 0
1✔
608
        LocalTrade.total_profit = 0
1✔
609

610
    def adjust_min_max_rates(self, current_price: float, current_price_low: float) -> None:
1✔
611
        """
612
        Adjust the max_rate and min_rate.
613
        """
614
        self.max_rate = max(current_price, self.max_rate or self.open_rate)
1✔
615
        self.min_rate = min(current_price_low, self.min_rate or self.open_rate)
1✔
616

617
    def set_liquidation_price(self, liquidation_price: Optional[float]):
1✔
618
        """
619
        Method you should use to set self.liquidation price.
620
        Assures stop_loss is not passed the liquidation price
621
        """
622
        if not liquidation_price:
1✔
623
            return
1✔
624
        self.liquidation_price = liquidation_price
1✔
625

626
    def __set_stop_loss(self, stop_loss: float, percent: float):
1✔
627
        """
628
        Method used internally to set self.stop_loss.
629
        """
630
        if not self.stop_loss:
1✔
631
            self.initial_stop_loss = stop_loss
1✔
632
        self.stop_loss = stop_loss
1✔
633

634
        self.stop_loss_pct = -1 * abs(percent)
1✔
635

636
    def adjust_stop_loss(self, current_price: float, stoploss: Optional[float],
1✔
637
                         initial: bool = False, allow_refresh: bool = False) -> None:
638
        """
639
        This adjusts the stop loss to it's most recently observed setting
640
        :param current_price: Current rate the asset is traded
641
        :param stoploss: Stoploss as factor (sample -0.05 -> -5% below current price).
642
        :param initial: Called to initiate stop_loss.
643
            Skips everything if self.stop_loss is already set.
644
        :param refresh: Called to refresh stop_loss, allows adjustment in both directions
645
        """
646
        if stoploss is None or (initial and not (self.stop_loss is None or self.stop_loss == 0)):
1✔
647
            # Don't modify if called with initial and nothing to do
648
            return
1✔
649

650
        leverage = self.leverage or 1.0
1✔
651
        if self.is_short:
1✔
652
            new_loss = float(current_price * (1 + abs(stoploss / leverage)))
1✔
653
        else:
654
            new_loss = float(current_price * (1 - abs(stoploss / leverage)))
1✔
655

656
        stop_loss_norm = price_to_precision(new_loss, self.price_precision, self.precision_mode,
1✔
657
                                            rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP)
658
        # no stop loss assigned yet
659
        if self.initial_stop_loss_pct is None:
1✔
660
            self.__set_stop_loss(stop_loss_norm, stoploss)
1✔
661
            self.initial_stop_loss = price_to_precision(
1✔
662
                stop_loss_norm, self.price_precision, self.precision_mode,
663
                rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP)
664
            self.initial_stop_loss_pct = -1 * abs(stoploss)
1✔
665

666
        # evaluate if the stop loss needs to be updated
667
        else:
668
            higher_stop = stop_loss_norm > self.stop_loss
1✔
669
            lower_stop = stop_loss_norm < self.stop_loss
1✔
670

671
            # stop losses only walk up, never down!,
672
            #   ? But adding more to a leveraged trade would create a lower liquidation price,
673
            #   ? decreasing the minimum stoploss
674
            if (
1✔
675
                allow_refresh
676
                or (higher_stop and not self.is_short)
677
                or (lower_stop and self.is_short)
678
            ):
679
                logger.debug(f"{self.pair} - Adjusting stoploss...")
1✔
680
                if not allow_refresh:
1✔
681
                    self.is_stop_loss_trailing = True
1✔
682
                self.__set_stop_loss(stop_loss_norm, stoploss)
1✔
683
            else:
684
                logger.debug(f"{self.pair} - Keeping current stoploss...")
1✔
685

686
        logger.debug(
1✔
687
            f"{self.pair} - Stoploss adjusted. current_price={current_price:.8f}, "
688
            f"open_rate={self.open_rate:.8f}, max_rate={self.max_rate or self.open_rate:.8f}, "
689
            f"initial_stop_loss={self.initial_stop_loss:.8f}, "
690
            f"stop_loss={self.stop_loss:.8f}. "
691
            f"Trailing stoploss saved us: "
692
            f"{float(self.stop_loss) - float(self.initial_stop_loss or 0.0):.8f}.")
693

694
    def update_trade(self, order: Order) -> None:
1✔
695
        """
696
        Updates this entity with amount and actual open/close rates.
697
        :param order: order retrieved by exchange.fetch_order()
698
        :return: None
699
        """
700

701
        # Ignore open and cancelled orders
702
        if order.status == 'open' or order.safe_price is None:
1✔
703
            return
1✔
704

705
        logger.info(f'Updating trade (id={self.id}) ...')
1✔
706

707
        if order.ft_order_side == self.entry_side:
1✔
708
            # Update open rate and actual amount
709
            self.open_rate = order.safe_price
1✔
710
            self.amount = order.safe_amount_after_fee
1✔
711
            if self.is_open:
1✔
712
                payment = "SELL" if self.is_short else "BUY"
1✔
713
                logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.')
1✔
714
            # condition to avoid reset value when updating fees
715
            if self.open_order_id == order.order_id:
1✔
716
                self.open_order_id = None
1✔
717
            else:
718
                logger.warning(
1✔
719
                    f'Got different open_order_id {self.open_order_id} != {order.order_id}')
720
            self.recalc_trade_from_orders()
1✔
721
        elif order.ft_order_side == self.exit_side:
1✔
722
            if self.is_open:
1✔
723
                payment = "BUY" if self.is_short else "SELL"
1✔
724
                # * On margin shorts, you buy a little bit more than the amount (amount + interest)
725
                logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.')
1✔
726
            # condition to avoid reset value when updating fees
727
            if self.open_order_id == order.order_id:
1✔
728
                self.open_order_id = None
1✔
729
            else:
730
                logger.warning(
1✔
731
                    f'Got different open_order_id {self.open_order_id} != {order.order_id}')
732

733
        elif order.ft_order_side == 'stoploss' and order.status not in ('open', ):
1✔
734
            self.stoploss_order_id = None
1✔
735
            self.close_rate_requested = self.stop_loss
1✔
736
            self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
1✔
737
            if self.is_open and order.safe_filled > 0:
1✔
738
                logger.info(f'{order.order_type.upper()} is hit for {self}.')
1✔
739
        else:
740
            raise ValueError(f'Unknown order type: {order.order_type}')
1✔
741

742
        if order.ft_order_side != self.entry_side:
1✔
743
            amount_tr = amount_to_contract_precision(self.amount, self.amount_precision,
1✔
744
                                                     self.precision_mode, self.contract_size)
745
            if (
1✔
746
                isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC)
747
                or order.safe_amount_after_fee > amount_tr
748
            ):
749
                self.close(order.safe_price)
1✔
750
            else:
751
                self.recalc_trade_from_orders()
1✔
752

753
        Trade.commit()
1✔
754

755
    def close(self, rate: float, *, show_msg: bool = True) -> None:
1✔
756
        """
757
        Sets close_rate to the given rate, calculates total profit
758
        and marks trade as closed
759
        """
760
        self.close_rate = rate
1✔
761
        self.close_date = self.close_date or datetime.utcnow()
1✔
762
        self.is_open = False
1✔
763
        self.exit_order_status = 'closed'
1✔
764
        self.open_order_id = None
1✔
765
        self.recalc_trade_from_orders(is_closing=True)
1✔
766
        if show_msg:
1✔
767
            logger.info(f"Marking {self} as closed as the trade is fulfilled "
1✔
768
                        "and found no open orders for it.")
769

770
    def update_fee(self, fee_cost: float, fee_currency: Optional[str], fee_rate: Optional[float],
1✔
771
                   side: str) -> None:
772
        """
773
        Update Fee parameters. Only acts once per side
774
        """
775
        if self.entry_side == side and self.fee_open_currency is None:
1✔
776
            self.fee_open_cost = fee_cost
1✔
777
            self.fee_open_currency = fee_currency
1✔
778
            if fee_rate is not None:
1✔
779
                self.fee_open = fee_rate
1✔
780
                # Assume close-fee will fall into the same fee category and take an educated guess
781
                self.fee_close = fee_rate
1✔
782
        elif self.exit_side == side and self.fee_close_currency is None:
1✔
783
            self.fee_close_cost = fee_cost
1✔
784
            self.fee_close_currency = fee_currency
1✔
785
            if fee_rate is not None:
1✔
786
                self.fee_close = fee_rate
1✔
787

788
    def fee_updated(self, side: str) -> bool:
1✔
789
        """
790
        Verify if this side (buy / sell) has already been updated
791
        """
792
        if self.entry_side == side:
1✔
793
            return self.fee_open_currency is not None
1✔
794
        elif self.exit_side == side:
1✔
795
            return self.fee_close_currency is not None
1✔
796
        else:
797
            return False
1✔
798

799
    def update_order(self, order: Dict) -> None:
1✔
800
        Order.update_orders(self.orders, order)
1✔
801

802
    def get_exit_order_count(self) -> int:
1✔
803
        """
804
        Get amount of failed exiting orders
805
        assumes full exits.
806
        """
807
        return len([o for o in self.orders if o.ft_order_side == self.exit_side])
1✔
808

809
    def _calc_open_trade_value(self, amount: float, open_rate: float) -> float:
1✔
810
        """
811
        Calculate the open_rate including open_fee.
812
        :return: Price in of the open trade incl. Fees
813
        """
814
        open_trade = FtPrecise(amount) * FtPrecise(open_rate)
1✔
815
        fees = open_trade * FtPrecise(self.fee_open)
1✔
816
        if self.is_short:
1✔
817
            return float(open_trade - fees)
1✔
818
        else:
819
            return float(open_trade + fees)
1✔
820

821
    def recalc_open_trade_value(self) -> None:
1✔
822
        """
823
        Recalculate open_trade_value.
824
        Must be called whenever open_rate, fee_open is changed.
825
        """
826
        self.open_trade_value = self._calc_open_trade_value(self.amount, self.open_rate)
1✔
827

828
    def calculate_interest(self) -> FtPrecise:
1✔
829
        """
830
        Calculate interest for this trade. Only applicable for Margin trading.
831
        """
832
        zero = FtPrecise(0.0)
1✔
833
        # If nothing was borrowed
834
        if self.trading_mode != TradingMode.MARGIN or self.has_no_leverage:
1✔
835
            return zero
1✔
836

837
        open_date = self.open_date.replace(tzinfo=None)
1✔
838
        now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None)
1✔
839
        sec_per_hour = FtPrecise(3600)
1✔
840
        total_seconds = FtPrecise((now - open_date).total_seconds())
1✔
841
        hours = total_seconds / sec_per_hour or zero
1✔
842

843
        rate = FtPrecise(self.interest_rate)
1✔
844
        borrowed = FtPrecise(self.borrowed)
1✔
845

846
        return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
1✔
847

848
    def _calc_base_close(self, amount: FtPrecise, rate: float, fee: Optional[float]) -> FtPrecise:
1✔
849

850
        close_trade = amount * FtPrecise(rate)
1✔
851
        fees = close_trade * FtPrecise(fee or 0.0)
1✔
852

853
        if self.is_short:
1✔
854
            return close_trade + fees
1✔
855
        else:
856
            return close_trade - fees
1✔
857

858
    def calc_close_trade_value(self, rate: float, amount: Optional[float] = None) -> float:
1✔
859
        """
860
        Calculate the Trade's close value including fees
861
        :param rate: rate to compare with.
862
        :return: value in stake currency of the open trade
863
        """
864
        if rate is None and not self.close_rate:
1✔
865
            return 0.0
1✔
866

867
        amount1 = FtPrecise(amount or self.amount)
1✔
868
        trading_mode = self.trading_mode or TradingMode.SPOT
1✔
869

870
        if trading_mode == TradingMode.SPOT:
1✔
871
            return float(self._calc_base_close(amount1, rate, self.fee_close))
1✔
872

873
        elif (trading_mode == TradingMode.MARGIN):
1✔
874

875
            total_interest = self.calculate_interest()
1✔
876

877
            if self.is_short:
1✔
878
                amount1 = amount1 + total_interest
1✔
879
                return float(self._calc_base_close(amount1, rate, self.fee_close))
1✔
880
            else:
881
                # Currency already owned for longs, no need to purchase
882
                return float(self._calc_base_close(amount1, rate, self.fee_close) - total_interest)
1✔
883

884
        elif (trading_mode == TradingMode.FUTURES):
1✔
885
            funding_fees = self.funding_fees or 0.0
1✔
886
            # Positive funding_fees -> Trade has gained from fees.
887
            # Negative funding_fees -> Trade had to pay the fees.
888
            if self.is_short:
1✔
889
                return float(self._calc_base_close(amount1, rate, self.fee_close)) - funding_fees
1✔
890
            else:
891
                return float(self._calc_base_close(amount1, rate, self.fee_close)) + funding_fees
1✔
892
        else:
893
            raise OperationalException(
×
894
                f"{self.trading_mode.value} trading is not yet available using freqtrade")
895

896
    def calc_profit(self, rate: float, amount: Optional[float] = None,
1✔
897
                    open_rate: Optional[float] = None) -> float:
898
        """
899
        Calculate the absolute profit in stake currency between Close and Open trade
900
        Deprecated - only available for backwards compatibility
901
        :param rate: close rate to compare with.
902
        :param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
903
        :param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
904
        :return: profit in stake currency as float
905
        """
906
        prof = self.calculate_profit(rate, amount, open_rate)
1✔
907
        return prof.profit_abs
1✔
908

909
    def calculate_profit(self, rate: float, amount: Optional[float] = None,
1✔
910
                         open_rate: Optional[float] = None) -> ProfitStruct:
911
        """
912
        Calculate profit metrics (absolute, ratio, total, total ratio).
913
        All calculations include fees.
914
        :param rate: close rate to compare with.
915
        :param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
916
        :param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
917
        :return: Profit structure, containing absolute and relative profits.
918
        """
919

920
        close_trade_value = self.calc_close_trade_value(rate, amount)
1✔
921
        if amount is None or open_rate is None:
1✔
922
            open_trade_value = self.open_trade_value
1✔
923
        else:
924
            open_trade_value = self._calc_open_trade_value(amount, open_rate)
1✔
925

926
        if self.is_short:
1✔
927
            profit_abs = open_trade_value - close_trade_value
1✔
928
        else:
929
            profit_abs = close_trade_value - open_trade_value
1✔
930

931
        try:
1✔
932
            if self.is_short:
1✔
933
                profit_ratio = (1 - (close_trade_value / open_trade_value)) * self.leverage
1✔
934
            else:
935
                profit_ratio = ((close_trade_value / open_trade_value) - 1) * self.leverage
1✔
936
            profit_ratio = float(f"{profit_ratio:.8f}")
1✔
937
        except ZeroDivisionError:
×
938
            profit_ratio = 0.0
×
939

940
        total_profit_abs = profit_abs + self.realized_profit
1✔
941
        total_profit_ratio = (
1✔
942
            (total_profit_abs / self.max_stake_amount) * self.leverage
943
            if self.max_stake_amount else 0.0
944
        )
945
        total_profit_ratio = float(f"{total_profit_ratio:.8f}")
1✔
946
        profit_abs = float(f"{profit_abs:.8f}")
1✔
947

948
        return ProfitStruct(
1✔
949
            profit_abs=profit_abs,
950
            profit_ratio=profit_ratio,
951
            total_profit=profit_abs + self.realized_profit,
952
            total_profit_ratio=total_profit_ratio,
953
        )
954

955
    def calc_profit_ratio(
1✔
956
            self, rate: float, amount: Optional[float] = None,
957
            open_rate: Optional[float] = None) -> float:
958
        """
959
        Calculates the profit as ratio (including fee).
960
        :param rate: rate to compare with.
961
        :param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
962
        :param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
963
        :return: profit ratio as float
964
        """
965
        close_trade_value = self.calc_close_trade_value(rate, amount)
1✔
966

967
        if amount is None or open_rate is None:
1✔
968
            open_trade_value = self.open_trade_value
1✔
969
        else:
970
            open_trade_value = self._calc_open_trade_value(amount, open_rate)
1✔
971

972
        short_close_zero = (self.is_short and close_trade_value == 0.0)
1✔
973
        long_close_zero = (not self.is_short and open_trade_value == 0.0)
1✔
974

975
        if (short_close_zero or long_close_zero):
1✔
976
            return 0.0
×
977
        else:
978
            if self.is_short:
1✔
979
                profit_ratio = (1 - (close_trade_value / open_trade_value)) * self.leverage
1✔
980
            else:
981
                profit_ratio = ((close_trade_value / open_trade_value) - 1) * self.leverage
1✔
982

983
        return float(f"{profit_ratio:.8f}")
1✔
984

985
    def recalc_trade_from_orders(self, *, is_closing: bool = False):
1✔
986
        ZERO = FtPrecise(0.0)
1✔
987
        current_amount = FtPrecise(0.0)
1✔
988
        current_stake = FtPrecise(0.0)
1✔
989
        max_stake_amount = FtPrecise(0.0)
1✔
990
        total_stake = 0.0  # Total stake after all buy orders (does not subtract!)
1✔
991
        avg_price = FtPrecise(0.0)
1✔
992
        close_profit = 0.0
1✔
993
        close_profit_abs = 0.0
1✔
994
        # Reset funding fees
995
        self.funding_fees = 0.0
1✔
996
        funding_fees = 0.0
1✔
997
        ordercount = len(self.orders) - 1
1✔
998
        for i, o in enumerate(self.orders):
1✔
999
            if o.ft_is_open or not o.filled:
1✔
1000
                continue
1✔
1001
            funding_fees += (o.funding_fee or 0.0)
1✔
1002
            tmp_amount = FtPrecise(o.safe_amount_after_fee)
1✔
1003
            tmp_price = FtPrecise(o.safe_price)
1✔
1004

1005
            is_exit = o.ft_order_side != self.entry_side
1✔
1006
            side = FtPrecise(-1 if is_exit else 1)
1✔
1007
            if tmp_amount > ZERO and tmp_price is not None:
1✔
1008
                current_amount += tmp_amount * side
1✔
1009
                price = avg_price if is_exit else tmp_price
1✔
1010
                current_stake += price * tmp_amount * side
1✔
1011

1012
                if current_amount > ZERO:
1✔
1013
                    avg_price = current_stake / current_amount
1✔
1014

1015
            if is_exit:
1✔
1016
                # Process exits
1017
                if i == ordercount and is_closing:
1✔
1018
                    # Apply funding fees only to the last closing order
1019
                    self.funding_fees = funding_fees
1✔
1020

1021
                exit_rate = o.safe_price
1✔
1022
                exit_amount = o.safe_amount_after_fee
1✔
1023
                prof = self.calculate_profit(exit_rate, exit_amount, float(avg_price))
1✔
1024
                close_profit_abs += prof.profit_abs
1✔
1025
                close_profit = prof.profit_ratio
1✔
1026
            else:
1027
                total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price)
1✔
1028
                max_stake_amount += (tmp_amount * price)
1✔
1029
        self.funding_fees = funding_fees
1✔
1030
        self.max_stake_amount = float(max_stake_amount)
1✔
1031

1032
        if close_profit:
1✔
1033
            self.close_profit = close_profit
1✔
1034
            self.realized_profit = close_profit_abs
1✔
1035
            self.close_profit_abs = prof.profit_abs
1✔
1036

1037
        current_amount_tr = amount_to_contract_precision(
1✔
1038
            float(current_amount), self.amount_precision, self.precision_mode, self.contract_size)
1039
        if current_amount_tr > 0.0:
1✔
1040
            # Trade is still open
1041
            # Leverage not updated, as we don't allow changing leverage through DCA at the moment.
1042
            self.open_rate = float(current_stake / current_amount)
1✔
1043
            self.amount = current_amount_tr
1✔
1044
            self.stake_amount = float(current_stake) / (self.leverage or 1.0)
1✔
1045
            self.fee_open_cost = self.fee_open * float(current_stake)
1✔
1046
            self.recalc_open_trade_value()
1✔
1047
            if self.stop_loss_pct is not None and self.open_rate is not None:
1✔
1048
                self.adjust_stop_loss(self.open_rate, self.stop_loss_pct)
1✔
1049
        elif is_closing and total_stake > 0:
1✔
1050
            # Close profit abs / maximum owned
1051
            # Fees are considered as they are part of close_profit_abs
1052
            self.close_profit = (close_profit_abs / total_stake) * self.leverage
1✔
1053
            self.close_profit_abs = close_profit_abs
1✔
1054

1055
    def select_order_by_order_id(self, order_id: str) -> Optional[Order]:
1✔
1056
        """
1057
        Finds order object by Order id.
1058
        :param order_id: Exchange order id
1059
        """
1060
        for o in self.orders:
1✔
1061
            if o.order_id == order_id:
1✔
1062
                return o
1✔
1063
        return None
1✔
1064

1065
    def select_order(self, order_side: Optional[str] = None,
1✔
1066
                     is_open: Optional[bool] = None, only_filled: bool = False) -> Optional[Order]:
1067
        """
1068
        Finds latest order for this orderside and status
1069
        :param order_side: ft_order_side of the order (either 'buy', 'sell' or 'stoploss')
1070
        :param is_open: Only search for open orders?
1071
        :param only_filled: Only search for Filled orders (only valid with is_open=False).
1072
        :return: latest Order object if it exists, else None
1073
        """
1074
        orders = self.orders
1✔
1075
        if order_side:
1✔
1076
            orders = [o for o in orders if o.ft_order_side == order_side]
1✔
1077
        if is_open is not None:
1✔
1078
            orders = [o for o in orders if o.ft_is_open == is_open]
1✔
1079
        if is_open is False and only_filled:
1✔
1080
            orders = [o for o in orders if o.filled and o.status in NON_OPEN_EXCHANGE_STATES]
1✔
1081
        if len(orders) > 0:
1✔
1082
            return orders[-1]
1✔
1083
        else:
1084
            return None
1✔
1085

1086
    def select_filled_orders(self, order_side: Optional[str] = None) -> List['Order']:
1✔
1087
        """
1088
        Finds filled orders for this order side.
1089
        Will not return open orders which already partially filled.
1090
        :param order_side: Side of the order (either 'buy', 'sell', or None)
1091
        :return: array of Order objects
1092
        """
1093
        return [o for o in self.orders if ((o.ft_order_side == order_side) or (order_side is None))
1✔
1094
                and o.ft_is_open is False
1095
                and o.filled
1096
                and o.status in NON_OPEN_EXCHANGE_STATES]
1097

1098
    def select_filled_or_open_orders(self) -> List['Order']:
1✔
1099
        """
1100
        Finds filled or open orders
1101
        :param order_side: Side of the order (either 'buy', 'sell', or None)
1102
        :return: array of Order objects
1103
        """
1104
        return [o for o in self.orders if
1✔
1105
                (
1106
                    o.ft_is_open is False
1107
                    and (o.filled or 0) > 0
1108
                    and o.status in NON_OPEN_EXCHANGE_STATES
1109
                    )
1110
                or (o.ft_is_open is True and o.status is not None)
1111
                ]
1112

1113
    @property
1✔
1114
    def nr_of_successful_entries(self) -> int:
1✔
1115
        """
1116
        Helper function to count the number of entry orders that have been filled.
1117
        :return: int count of entry orders that have been filled for this trade.
1118
        """
1119

1120
        return len(self.select_filled_orders(self.entry_side))
1✔
1121

1122
    @property
1✔
1123
    def nr_of_successful_exits(self) -> int:
1✔
1124
        """
1125
        Helper function to count the number of exit orders that have been filled.
1126
        :return: int count of exit orders that have been filled for this trade.
1127
        """
1128
        return len(self.select_filled_orders(self.exit_side))
1✔
1129

1130
    @property
1✔
1131
    def nr_of_successful_buys(self) -> int:
1✔
1132
        """
1133
        Helper function to count the number of buy orders that have been filled.
1134
        WARNING: Please use nr_of_successful_entries for short support.
1135
        :return: int count of buy orders that have been filled for this trade.
1136
        """
1137

1138
        return len(self.select_filled_orders('buy'))
1✔
1139

1140
    @property
1✔
1141
    def nr_of_successful_sells(self) -> int:
1✔
1142
        """
1143
        Helper function to count the number of sell orders that have been filled.
1144
        WARNING: Please use nr_of_successful_exits for short support.
1145
        :return: int count of sell orders that have been filled for this trade.
1146
        """
1147
        return len(self.select_filled_orders('sell'))
×
1148

1149
    @property
1✔
1150
    def sell_reason(self) -> Optional[str]:
1✔
1151
        """ DEPRECATED! Please use exit_reason instead."""
1152
        return self.exit_reason
1✔
1153

1154
    @property
1✔
1155
    def safe_close_rate(self) -> float:
1✔
1156
        return self.close_rate or self.close_rate_requested or 0.0
1✔
1157

1158
    @staticmethod
1✔
1159
    def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None,
1✔
1160
                         open_date: Optional[datetime] = None,
1161
                         close_date: Optional[datetime] = None,
1162
                         ) -> List['LocalTrade']:
1163
        """
1164
        Helper function to query Trades.
1165
        Returns a List of trades, filtered on the parameters given.
1166
        In live mode, converts the filter to a database query and returns all rows
1167
        In Backtest mode, uses filters on Trade.trades to get the result.
1168

1169
        :param pair: Filter by pair
1170
        :param is_open: Filter by open/closed status
1171
        :param open_date: Filter by open_date (filters via trade.open_date > input)
1172
        :param close_date: Filter by close_date (filters via trade.close_date > input)
1173
                           Will implicitly only return closed trades.
1174
        :return: unsorted List[Trade]
1175
        """
1176

1177
        # Offline mode - without database
1178
        if is_open is not None:
1✔
1179
            if is_open:
1✔
1180
                sel_trades = LocalTrade.trades_open
1✔
1181
            else:
1182
                sel_trades = LocalTrade.trades
1✔
1183

1184
        else:
1185
            # Not used during backtesting, but might be used by a strategy
1186
            sel_trades = list(LocalTrade.trades + LocalTrade.trades_open)
1✔
1187

1188
        if pair:
1✔
1189
            sel_trades = [trade for trade in sel_trades if trade.pair == pair]
1✔
1190
        if open_date:
1✔
1191
            sel_trades = [trade for trade in sel_trades if trade.open_date > open_date]
1✔
1192
        if close_date:
1✔
1193
            sel_trades = [trade for trade in sel_trades if trade.close_date
1✔
1194
                          and trade.close_date > close_date]
1195

1196
        return sel_trades
1✔
1197

1198
    @staticmethod
1✔
1199
    def close_bt_trade(trade):
1✔
1200
        LocalTrade.trades_open.remove(trade)
1✔
1201
        LocalTrade.bt_trades_open_pp[trade.pair].remove(trade)
1✔
1202
        LocalTrade.bt_open_open_trade_count -= 1
1✔
1203
        LocalTrade.trades.append(trade)
1✔
1204
        LocalTrade.total_profit += trade.close_profit_abs
1✔
1205

1206
    @staticmethod
1✔
1207
    def add_bt_trade(trade):
1✔
1208
        if trade.is_open:
1✔
1209
            LocalTrade.trades_open.append(trade)
1✔
1210
            LocalTrade.bt_trades_open_pp[trade.pair].append(trade)
1✔
1211
            LocalTrade.bt_open_open_trade_count += 1
1✔
1212
        else:
1213
            LocalTrade.trades.append(trade)
1✔
1214

1215
    @staticmethod
1✔
1216
    def remove_bt_trade(trade):
1✔
1217
        LocalTrade.trades_open.remove(trade)
1✔
1218
        LocalTrade.bt_trades_open_pp[trade.pair].remove(trade)
1✔
1219
        LocalTrade.bt_open_open_trade_count -= 1
1✔
1220

1221
    @staticmethod
1✔
1222
    def get_open_trades() -> List[Any]:
1✔
1223
        """
1224
        Retrieve open trades
1225
        """
1226
        return Trade.get_trades_proxy(is_open=True)
1✔
1227

1228
    @staticmethod
1✔
1229
    def get_open_trade_count() -> int:
1✔
1230
        """
1231
        get open trade count
1232
        """
1233
        if Trade.use_db:
1✔
1234
            return Trade.session.execute(
1✔
1235
                select(func.count(Trade.id)).filter(Trade.is_open.is_(True))
1236
            ).scalar_one()
1237
        else:
1238
            return LocalTrade.bt_open_open_trade_count
1✔
1239

1240
    @staticmethod
1✔
1241
    def stoploss_reinitialization(desired_stoploss: float):
1✔
1242
        """
1243
        Adjust initial Stoploss to desired stoploss for all open trades.
1244
        """
1245
        trade: Trade
1246
        for trade in Trade.get_open_trades():
1✔
1247
            logger.info(f"Found open trade: {trade}")
1✔
1248

1249
            # skip case if trailing-stop changed the stoploss already.
1250
            if (not trade.is_stop_loss_trailing
1✔
1251
                    and trade.initial_stop_loss_pct != desired_stoploss):
1252
                # Stoploss value got changed
1253

1254
                logger.info(f"Stoploss for {trade} needs adjustment...")
1✔
1255
                # Force reset of stoploss
1256
                trade.stop_loss = 0.0
1✔
1257
                trade.initial_stop_loss_pct = None
1✔
1258
                trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
1✔
1259
                logger.info(f"New stoploss: {trade.stop_loss}.")
1✔
1260

1261

1262
class Trade(ModelBase, LocalTrade):
1✔
1263
    """
1264
    Trade database model.
1265
    Also handles updating and querying trades
1266

1267
    Note: Fields must be aligned with LocalTrade class
1268
    """
1269
    __tablename__ = 'trades'
1✔
1270
    session: ClassVar[SessionType]
1✔
1271

1272
    use_db: bool = True
1✔
1273

1274
    id: Mapped[int] = mapped_column(Integer, primary_key=True)  # type: ignore
1✔
1275

1276
    orders: Mapped[List[Order]] = relationship(
1✔
1277
        "Order", order_by="Order.id", cascade="all, delete-orphan", lazy="selectin",
1278
        innerjoin=True)  # type: ignore
1279

1280
    exchange: Mapped[str] = mapped_column(String(25), nullable=False)  # type: ignore
1✔
1281
    pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True)  # type: ignore
1✔
1282
    base_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True)  # type: ignore
1✔
1283
    stake_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True)  # type: ignore
1✔
1284
    is_open: Mapped[bool] = mapped_column(nullable=False, default=True, index=True)  # type: ignore
1✔
1285
    fee_open: Mapped[float] = mapped_column(Float(), nullable=False, default=0.0)  # type: ignore
1✔
1286
    fee_open_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)  # type: ignore
1✔
1287
    fee_open_currency: Mapped[Optional[str]] = mapped_column(
1✔
1288
        String(25), nullable=True)  # type: ignore
1289
    fee_close: Mapped[Optional[float]] = mapped_column(
1✔
1290
        Float(), nullable=False, default=0.0)  # type: ignore
1291
    fee_close_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)  # type: ignore
1✔
1292
    fee_close_currency: Mapped[Optional[str]] = mapped_column(
1✔
1293
        String(25), nullable=True)  # type: ignore
1294
    open_rate: Mapped[float] = mapped_column(Float())  # type: ignore
1✔
1295
    open_rate_requested: Mapped[Optional[float]] = mapped_column(
1✔
1296
        Float(), nullable=True)  # type: ignore
1297
    # open_trade_value - calculated via _calc_open_trade_value
1298
    open_trade_value: Mapped[float] = mapped_column(Float(), nullable=True)  # type: ignore
1✔
1299
    close_rate: Mapped[Optional[float]] = mapped_column(Float())  # type: ignore
1✔
1300
    close_rate_requested: Mapped[Optional[float]] = mapped_column(Float())  # type: ignore
1✔
1301
    realized_profit: Mapped[float] = mapped_column(
1✔
1302
        Float(), default=0.0, nullable=True)  # type: ignore
1303
    close_profit: Mapped[Optional[float]] = mapped_column(Float())  # type: ignore
1✔
1304
    close_profit_abs: Mapped[Optional[float]] = mapped_column(Float())  # type: ignore
1✔
1305
    stake_amount: Mapped[float] = mapped_column(Float(), nullable=False)  # type: ignore
1✔
1306
    max_stake_amount: Mapped[Optional[float]] = mapped_column(Float())  # type: ignore
1✔
1307
    amount: Mapped[float] = mapped_column(Float())  # type: ignore
1✔
1308
    amount_requested: Mapped[Optional[float]] = mapped_column(Float())  # type: ignore
1✔
1309
    open_date: Mapped[datetime] = mapped_column(
1✔
1310
        nullable=False, default=datetime.utcnow)  # type: ignore
1311
    close_date: Mapped[Optional[datetime]] = mapped_column()  # type: ignore
1✔
1312
    open_order_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)  # type: ignore
1✔
1313
    # absolute value of the stop loss
1314
    stop_loss: Mapped[float] = mapped_column(Float(), nullable=True, default=0.0)  # type: ignore
1✔
1315
    # percentage value of the stop loss
1316
    stop_loss_pct: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)  # type: ignore
1✔
1317
    # absolute value of the initial stop loss
1318
    initial_stop_loss: Mapped[Optional[float]] = mapped_column(
1✔
1319
        Float(), nullable=True, default=0.0)  # type: ignore
1320
    # percentage value of the initial stop loss
1321
    initial_stop_loss_pct: Mapped[Optional[float]] = mapped_column(
1✔
1322
        Float(), nullable=True)  # type: ignore
1323
    is_stop_loss_trailing: Mapped[bool] = mapped_column(
1✔
1324
        nullable=False, default=False)  # type: ignore
1325
    # stoploss order id which is on exchange
1326
    stoploss_order_id: Mapped[Optional[str]] = mapped_column(
1✔
1327
        String(255), nullable=True, index=True)  # type: ignore
1328
    # last update time of the stoploss order on exchange
1329
    stoploss_last_update: Mapped[Optional[datetime]] = mapped_column(nullable=True)  # type: ignore
1✔
1330
    # absolute value of the highest reached price
1331
    max_rate: Mapped[Optional[float]] = mapped_column(
1✔
1332
        Float(), nullable=True, default=0.0)  # type: ignore
1333
    # Lowest price reached
1334
    min_rate: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)  # type: ignore
1✔
1335
    exit_reason: Mapped[Optional[str]] = mapped_column(
1✔
1336
        String(CUSTOM_TAG_MAX_LENGTH), nullable=True)  # type: ignore
1337
    exit_order_status: Mapped[Optional[str]] = mapped_column(
1✔
1338
        String(100), nullable=True)  # type: ignore
1339
    strategy: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)  # type: ignore
1✔
1340
    enter_tag: Mapped[Optional[str]] = mapped_column(
1✔
1341
        String(CUSTOM_TAG_MAX_LENGTH), nullable=True)  # type: ignore
1342
    timeframe: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)  # type: ignore
1✔
1343

1344
    trading_mode: Mapped[TradingMode] = mapped_column(
1✔
1345
        Enum(TradingMode), nullable=True)  # type: ignore
1346
    amount_precision: Mapped[Optional[float]] = mapped_column(
1✔
1347
        Float(), nullable=True)  # type: ignore
1348
    price_precision: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)  # type: ignore
1✔
1349
    precision_mode: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)  # type: ignore
1✔
1350
    contract_size: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)  # type: ignore
1✔
1351

1352
    # Leverage trading properties
1353
    leverage: Mapped[float] = mapped_column(Float(), nullable=True, default=1.0)  # type: ignore
1✔
1354
    is_short: Mapped[bool] = mapped_column(nullable=False, default=False)  # type: ignore
1✔
1355
    liquidation_price: Mapped[Optional[float]] = mapped_column(
1✔
1356
        Float(), nullable=True)  # type: ignore
1357

1358
    # Margin Trading Properties
1359
    interest_rate: Mapped[float] = mapped_column(
1✔
1360
        Float(), nullable=False, default=0.0)  # type: ignore
1361

1362
    # Futures properties
1363
    funding_fees: Mapped[Optional[float]] = mapped_column(
1✔
1364
        Float(), nullable=True, default=None)  # type: ignore
1365

1366
    def __init__(self, **kwargs):
1✔
1367
        from_json = kwargs.pop('__FROM_JSON', None)
1✔
1368
        super().__init__(**kwargs)
1✔
1369
        if not from_json:
1✔
1370
            # Skip recalculation when loading from json
1371
            self.realized_profit = 0
1✔
1372
            self.recalc_open_trade_value()
1✔
1373

1374
    @validates('enter_tag', 'exit_reason')
1✔
1375
    def validate_string_len(self, key, value):
1✔
1376
        max_len = getattr(self.__class__, key).prop.columns[0].type.length
1✔
1377
        if value and len(value) > max_len:
1✔
1378
            return value[:max_len]
1✔
1379
        return value
1✔
1380

1381
    def delete(self) -> None:
1✔
1382

1383
        for order in self.orders:
1✔
1384
            Order.session.delete(order)
1✔
1385

1386
        Trade.session.delete(self)
1✔
1387
        Trade.commit()
1✔
1388

1389
    @staticmethod
1✔
1390
    def commit():
1✔
1391
        Trade.session.commit()
1✔
1392

1393
    @staticmethod
1✔
1394
    def rollback():
1✔
1395
        Trade.session.rollback()
1✔
1396

1397
    @staticmethod
1✔
1398
    def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None,
1✔
1399
                         open_date: Optional[datetime] = None,
1400
                         close_date: Optional[datetime] = None,
1401
                         ) -> List['LocalTrade']:
1402
        """
1403
        Helper function to query Trades.j
1404
        Returns a List of trades, filtered on the parameters given.
1405
        In live mode, converts the filter to a database query and returns all rows
1406
        In Backtest mode, uses filters on Trade.trades to get the result.
1407

1408
        :return: unsorted List[Trade]
1409
        """
1410
        if Trade.use_db:
1✔
1411
            trade_filter = []
1✔
1412
            if pair:
1✔
1413
                trade_filter.append(Trade.pair == pair)
1✔
1414
            if open_date:
1✔
1415
                trade_filter.append(Trade.open_date > open_date)
1✔
1416
            if close_date:
1✔
1417
                trade_filter.append(Trade.close_date > close_date)
1✔
1418
            if is_open is not None:
1✔
1419
                trade_filter.append(Trade.is_open.is_(is_open))
1✔
1420
            return cast(List[LocalTrade], Trade.get_trades(trade_filter).all())
1✔
1421
        else:
1422
            return LocalTrade.get_trades_proxy(
1✔
1423
                pair=pair, is_open=is_open,
1424
                open_date=open_date,
1425
                close_date=close_date
1426
            )
1427

1428
    @staticmethod
1✔
1429
    def get_trades_query(trade_filter=None, include_orders: bool = True) -> Select:
1✔
1430
        """
1431
        Helper function to query Trades using filters.
1432
        NOTE: Not supported in Backtesting.
1433
        :param trade_filter: Optional filter to apply to trades
1434
                             Can be either a Filter object, or a List of filters
1435
                             e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])`
1436
                             e.g. `(trade_filter=Trade.id == trade_id)`
1437
        :return: unsorted query object
1438
        """
1439
        if not Trade.use_db:
1✔
1440
            raise NotImplementedError('`Trade.get_trades()` not supported in backtesting mode.')
1✔
1441
        if trade_filter is not None:
1✔
1442
            if not isinstance(trade_filter, list):
1✔
1443
                trade_filter = [trade_filter]
1✔
1444
            this_query = select(Trade).filter(*trade_filter)
1✔
1445
        else:
1446
            this_query = select(Trade)
1✔
1447
        if not include_orders:
1✔
1448
            # Don't load order relations
1449
            # Consider using noload or raiseload instead of lazyload
1450
            this_query = this_query.options(lazyload(Trade.orders))
1✔
1451
        return this_query
1✔
1452

1453
    @staticmethod
1✔
1454
    def get_trades(trade_filter=None, include_orders: bool = True) -> ScalarResult['Trade']:
1✔
1455
        """
1456
        Helper function to query Trades using filters.
1457
        NOTE: Not supported in Backtesting.
1458
        :param trade_filter: Optional filter to apply to trades
1459
                             Can be either a Filter object, or a List of filters
1460
                             e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])`
1461
                             e.g. `(trade_filter=Trade.id == trade_id)`
1462
        :return: unsorted query object
1463
        """
1464
        query = Trade.get_trades_query(trade_filter, include_orders)
1✔
1465
        # this sholud remain split. if use_db is False, session is not available and the above will
1466
        # raise an exception.
1467
        return Trade.session.scalars(query)
1✔
1468

1469
    @staticmethod
1✔
1470
    def get_open_order_trades() -> List['Trade']:
1✔
1471
        """
1472
        Returns all open trades
1473
        NOTE: Not supported in Backtesting.
1474
        """
1475
        return cast(List[Trade], Trade.get_trades(Trade.open_order_id.isnot(None)).all())
1✔
1476

1477
    @staticmethod
1✔
1478
    def get_open_trades_without_assigned_fees():
1✔
1479
        """
1480
        Returns all open trades which don't have open fees set correctly
1481
        NOTE: Not supported in Backtesting.
1482
        """
1483
        return Trade.get_trades([Trade.fee_open_currency.is_(None),
1✔
1484
                                 Trade.orders.any(),
1485
                                 Trade.is_open.is_(True),
1486
                                 ]).all()
1487

1488
    @staticmethod
1✔
1489
    def get_closed_trades_without_assigned_fees():
1✔
1490
        """
1491
        Returns all closed trades which don't have fees set correctly
1492
        NOTE: Not supported in Backtesting.
1493
        """
1494
        return Trade.get_trades([Trade.fee_close_currency.is_(None),
1✔
1495
                                 Trade.orders.any(),
1496
                                 Trade.is_open.is_(False),
1497
                                 ]).all()
1498

1499
    @staticmethod
1✔
1500
    def get_total_closed_profit() -> float:
1✔
1501
        """
1502
        Retrieves total realized profit
1503
        """
1504
        if Trade.use_db:
1✔
1505
            total_profit: float = Trade.session.execute(
1✔
1506
                select(func.sum(Trade.close_profit_abs)).filter(Trade.is_open.is_(False))
1507
            ).scalar_one()
1508
        else:
1509
            total_profit = sum(t.close_profit_abs  # type: ignore
1✔
1510
                               for t in LocalTrade.get_trades_proxy(is_open=False))
1511
        return total_profit or 0
1✔
1512

1513
    @staticmethod
1✔
1514
    def total_open_trades_stakes() -> float:
1✔
1515
        """
1516
        Calculates total invested amount in open trades
1517
        in stake currency
1518
        """
1519
        if Trade.use_db:
1✔
1520
            total_open_stake_amount = Trade.session.scalar(
1✔
1521
                select(func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True))
1522
            )
1523
        else:
1524
            total_open_stake_amount = sum(
1✔
1525
                t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True))
1526
        return total_open_stake_amount or 0
1✔
1527

1528
    @staticmethod
1✔
1529
    def get_overall_performance(minutes=None) -> List[Dict[str, Any]]:
1✔
1530
        """
1531
        Returns List of dicts containing all Trades, including profit and trade count
1532
        NOTE: Not supported in Backtesting.
1533
        """
1534
        filters: List = [Trade.is_open.is_(False)]
1✔
1535
        if minutes:
1✔
1536
            start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes)
1✔
1537
            filters.append(Trade.close_date >= start_date)
1✔
1538

1539
        pair_rates = Trade.session.execute(
1✔
1540
            select(
1541
                Trade.pair,
1542
                func.sum(Trade.close_profit).label('profit_sum'),
1543
                func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
1544
                func.count(Trade.pair).label('count')
1545
            ).filter(*filters)
1546
            .group_by(Trade.pair)
1547
            .order_by(desc('profit_sum_abs'))
1548
            ).all()
1549

1550
        return [
1✔
1551
            {
1552
                'pair': pair,
1553
                'profit_ratio': profit,
1554
                'profit': round(profit * 100, 2),  # Compatibility mode
1555
                'profit_pct': round(profit * 100, 2),
1556
                'profit_abs': profit_abs,
1557
                'count': count
1558
            }
1559
            for pair, profit, profit_abs, count in pair_rates
1560
        ]
1561

1562
    @staticmethod
1✔
1563
    def get_enter_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
1564
        """
1565
        Returns List of dicts containing all Trades, based on buy tag performance
1566
        Can either be average for all pairs or a specific pair provided
1567
        NOTE: Not supported in Backtesting.
1568
        """
1569

1570
        filters: List = [Trade.is_open.is_(False)]
1✔
1571
        if (pair is not None):
1✔
1572
            filters.append(Trade.pair == pair)
1✔
1573

1574
        enter_tag_perf = Trade.session.execute(
1✔
1575
            select(
1576
                Trade.enter_tag,
1577
                func.sum(Trade.close_profit).label('profit_sum'),
1578
                func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
1579
                func.count(Trade.pair).label('count')
1580
            ).filter(*filters)
1581
            .group_by(Trade.enter_tag)
1582
            .order_by(desc('profit_sum_abs'))
1583
        ).all()
1584

1585
        return [
1✔
1586
            {
1587
                'enter_tag': enter_tag if enter_tag is not None else "Other",
1588
                'profit_ratio': profit,
1589
                'profit_pct': round(profit * 100, 2),
1590
                'profit_abs': profit_abs,
1591
                'count': count
1592
            }
1593
            for enter_tag, profit, profit_abs, count in enter_tag_perf
1594
        ]
1595

1596
    @staticmethod
1✔
1597
    def get_exit_reason_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
1598
        """
1599
        Returns List of dicts containing all Trades, based on exit reason performance
1600
        Can either be average for all pairs or a specific pair provided
1601
        NOTE: Not supported in Backtesting.
1602
        """
1603

1604
        filters: List = [Trade.is_open.is_(False)]
1✔
1605
        if (pair is not None):
1✔
1606
            filters.append(Trade.pair == pair)
1✔
1607
        sell_tag_perf = Trade.session.execute(
1✔
1608
            select(
1609
                Trade.exit_reason,
1610
                func.sum(Trade.close_profit).label('profit_sum'),
1611
                func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
1612
                func.count(Trade.pair).label('count')
1613
            ).filter(*filters)
1614
            .group_by(Trade.exit_reason)
1615
            .order_by(desc('profit_sum_abs'))
1616
        ).all()
1617

1618
        return [
1✔
1619
            {
1620
                'exit_reason': exit_reason if exit_reason is not None else "Other",
1621
                'profit_ratio': profit,
1622
                'profit_pct': round(profit * 100, 2),
1623
                'profit_abs': profit_abs,
1624
                'count': count
1625
            }
1626
            for exit_reason, profit, profit_abs, count in sell_tag_perf
1627
        ]
1628

1629
    @staticmethod
1✔
1630
    def get_mix_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
1631
        """
1632
        Returns List of dicts containing all Trades, based on entry_tag + exit_reason performance
1633
        Can either be average for all pairs or a specific pair provided
1634
        NOTE: Not supported in Backtesting.
1635
        """
1636

1637
        filters: List = [Trade.is_open.is_(False)]
1✔
1638
        if (pair is not None):
1✔
1639
            filters.append(Trade.pair == pair)
1✔
1640
        mix_tag_perf = Trade.session.execute(
1✔
1641
            select(
1642
                Trade.id,
1643
                Trade.enter_tag,
1644
                Trade.exit_reason,
1645
                func.sum(Trade.close_profit).label('profit_sum'),
1646
                func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
1647
                func.count(Trade.pair).label('count')
1648
            ).filter(*filters)
1649
            .group_by(Trade.id)
1650
            .order_by(desc('profit_sum_abs'))
1651
        ).all()
1652

1653
        return_list: List[Dict] = []
1✔
1654
        for id, enter_tag, exit_reason, profit, profit_abs, count in mix_tag_perf:
1✔
1655
            enter_tag = enter_tag if enter_tag is not None else "Other"
1✔
1656
            exit_reason = exit_reason if exit_reason is not None else "Other"
1✔
1657

1658
            if (exit_reason is not None and enter_tag is not None):
1✔
1659
                mix_tag = enter_tag + " " + exit_reason
1✔
1660
                i = 0
1✔
1661
                if not any(item["mix_tag"] == mix_tag for item in return_list):
1✔
1662
                    return_list.append({'mix_tag': mix_tag,
1✔
1663
                                        'profit': profit,
1664
                                        'profit_pct': round(profit * 100, 2),
1665
                                        'profit_abs': profit_abs,
1666
                                        'count': count})
1667
                else:
1668
                    while i < len(return_list):
×
1669
                        if return_list[i]["mix_tag"] == mix_tag:
×
1670
                            return_list[i] = {
×
1671
                                'mix_tag': mix_tag,
1672
                                'profit': profit + return_list[i]["profit"],
1673
                                'profit_pct': round(profit + return_list[i]["profit"] * 100, 2),
1674
                                'profit_abs': profit_abs + return_list[i]["profit_abs"],
1675
                                'count': 1 + return_list[i]["count"]}
1676
                        i += 1
×
1677

1678
        return return_list
1✔
1679

1680
    @staticmethod
1✔
1681
    def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)):
1✔
1682
        """
1683
        Get best pair with closed trade.
1684
        NOTE: Not supported in Backtesting.
1685
        :returns: Tuple containing (pair, profit_sum)
1686
        """
1687
        best_pair = Trade.session.execute(
1✔
1688
            select(
1689
                Trade.pair,
1690
                func.sum(Trade.close_profit).label('profit_sum')
1691
            ).filter(Trade.is_open.is_(False) & (Trade.close_date >= start_date))
1692
            .group_by(Trade.pair)
1693
            .order_by(desc('profit_sum'))
1694
        ).first()
1695

1696
        return best_pair
1✔
1697

1698
    @staticmethod
1✔
1699
    def get_trading_volume(start_date: datetime = datetime.fromtimestamp(0)) -> float:
1✔
1700
        """
1701
        Get Trade volume based on Orders
1702
        NOTE: Not supported in Backtesting.
1703
        :returns: Tuple containing (pair, profit_sum)
1704
        """
1705
        trading_volume = Trade.session.execute(
1✔
1706
            select(
1707
                func.sum(Order.cost).label('volume')
1708
            ).filter(
1709
                Order.order_filled_date >= start_date,
1710
                Order.status == 'closed'
1711
            )).scalar_one()
1712
        return trading_volume
1✔
1713

1714
    @classmethod
1✔
1715
    def from_json(cls, json_str: str) -> Self:
1✔
1716
        """
1717
        Create a Trade instance from a json string.
1718

1719
        Used for debugging purposes - please keep.
1720
        :param json_str: json string to parse
1721
        :return: Trade instance
1722
        """
1723
        import rapidjson
1✔
1724
        data = rapidjson.loads(json_str)
1✔
1725
        trade = cls(
1✔
1726
            __FROM_JSON=True,
1727
            id=data["trade_id"],
1728
            pair=data["pair"],
1729
            base_currency=data["base_currency"],
1730
            stake_currency=data["quote_currency"],
1731
            is_open=data["is_open"],
1732
            exchange=data["exchange"],
1733
            amount=data["amount"],
1734
            amount_requested=data["amount_requested"],
1735
            stake_amount=data["stake_amount"],
1736
            strategy=data["strategy"],
1737
            enter_tag=data["enter_tag"],
1738
            timeframe=data["timeframe"],
1739
            fee_open=data["fee_open"],
1740
            fee_open_cost=data["fee_open_cost"],
1741
            fee_open_currency=data["fee_open_currency"],
1742
            fee_close=data["fee_close"],
1743
            fee_close_cost=data["fee_close_cost"],
1744
            fee_close_currency=data["fee_close_currency"],
1745
            open_date=datetime.fromtimestamp(data["open_timestamp"] // 1000, tz=timezone.utc),
1746
            open_rate=data["open_rate"],
1747
            open_rate_requested=data["open_rate_requested"],
1748
            open_trade_value=data["open_trade_value"],
1749
            close_date=(datetime.fromtimestamp(data["close_timestamp"] // 1000, tz=timezone.utc)
1750
                        if data["close_timestamp"] else None),
1751
            realized_profit=data["realized_profit"],
1752
            close_rate=data["close_rate"],
1753
            close_rate_requested=data["close_rate_requested"],
1754
            close_profit=data["close_profit"],
1755
            close_profit_abs=data["close_profit_abs"],
1756
            exit_reason=data["exit_reason"],
1757
            exit_order_status=data["exit_order_status"],
1758
            stop_loss=data["stop_loss_abs"],
1759
            stop_loss_pct=data["stop_loss_ratio"],
1760
            stoploss_order_id=data["stoploss_order_id"],
1761
            stoploss_last_update=(
1762
                datetime.fromtimestamp(data["stoploss_last_update_timestamp"] // 1000,
1763
                                       tz=timezone.utc)
1764
                if data["stoploss_last_update_timestamp"] else None),
1765
            initial_stop_loss=data["initial_stop_loss_abs"],
1766
            initial_stop_loss_pct=data["initial_stop_loss_ratio"],
1767
            min_rate=data["min_rate"],
1768
            max_rate=data["max_rate"],
1769
            leverage=data["leverage"],
1770
            interest_rate=data["interest_rate"],
1771
            liquidation_price=data["liquidation_price"],
1772
            is_short=data["is_short"],
1773
            trading_mode=data["trading_mode"],
1774
            funding_fees=data["funding_fees"],
1775
            open_order_id=data["open_order_id"],
1776
        )
1777
        for order in data["orders"]:
1✔
1778

1779
            order_obj = Order(
1✔
1780
                amount=order["amount"],
1781
                ft_amount=order["amount"],
1782
                ft_order_side=order["ft_order_side"],
1783
                ft_pair=order["pair"],
1784
                ft_is_open=order["is_open"],
1785
                order_id=order["order_id"],
1786
                status=order["status"],
1787
                average=order["average"],
1788
                cost=order["cost"],
1789
                filled=order["filled"],
1790
                order_date=datetime.strptime(order["order_date"], DATETIME_PRINT_FORMAT),
1791
                order_filled_date=(datetime.fromtimestamp(
1792
                    order["order_filled_timestamp"] // 1000, tz=timezone.utc)
1793
                    if order["order_filled_timestamp"] else None),
1794
                order_type=order["order_type"],
1795
                price=order["price"],
1796
                ft_price=order["price"],
1797
                remaining=order["remaining"],
1798
            )
1799
            trade.orders.append(order_obj)
1✔
1800

1801
        return trade
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