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

freqtrade / freqtrade / 9394559170

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

push

github

xmatthias
Loader should be passed as kwarg for clarity

20280 of 21425 relevant lines covered (94.66%)

0.95 hits per line

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

97.47
/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 (CANCELED_EXCHANGE_STATES, CUSTOM_TAG_MAX_LENGTH,
1✔
17
                                 DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES,
18
                                 BuySell, LongShort)
19
from freqtrade.enums import ExitType, TradingMode
1✔
20
from freqtrade.exceptions import DependencyException, OperationalException
1✔
21
from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision,
1✔
22
                                price_to_precision)
23
from freqtrade.leverage import interest
1✔
24
from freqtrade.misc import safe_value_fallback
1✔
25
from freqtrade.persistence.base import ModelBase, SessionType
1✔
26
from freqtrade.persistence.custom_data import CustomDataWrapper, _CustomData
1✔
27
from freqtrade.util import FtPrecise, dt_from_ts, dt_now, dt_ts, dt_ts_none
1✔
28

29

30
logger = logging.getLogger(__name__)
1✔
31

32

33
@dataclass
1✔
34
class ProfitStruct:
1✔
35
    profit_abs: float
1✔
36
    profit_ratio: float
1✔
37
    total_profit: float
1✔
38
    total_profit_ratio: float
1✔
39

40

41
class Order(ModelBase):
1✔
42
    """
43
    Order database model
44
    Keeps a record of all orders placed on the exchange
45

46
    One to many relationship with Trades:
47
      - One trade can have many orders
48
      - One Order can only be associated with one Trade
49

50
    Mirrors CCXT Order structure
51
    """
52
    __tablename__ = 'orders'
1✔
53
    __allow_unmapped__ = True
1✔
54
    session: ClassVar[SessionType]
1✔
55

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

60
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
1✔
61
    ft_trade_id: Mapped[int] = mapped_column(Integer, ForeignKey('trades.id'), index=True)
1✔
62

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

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

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

91
    ft_fee_base: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
1✔
92
    ft_order_tag: Mapped[Optional[str]] = mapped_column(String(CUSTOM_TAG_MAX_LENGTH),
1✔
93
                                                        nullable=True)
94

95
    @property
1✔
96
    def order_date_utc(self) -> datetime:
1✔
97
        """ Order-date with UTC timezoneinfo"""
98
        return self.order_date.replace(tzinfo=timezone.utc)
1✔
99

100
    @property
1✔
101
    def order_filled_utc(self) -> Optional[datetime]:
1✔
102
        """ last order-date with UTC timezoneinfo"""
103
        return (
1✔
104
            self.order_filled_date.replace(tzinfo=timezone.utc) if self.order_filled_date else None
105
        )
106

107
    @property
1✔
108
    def safe_amount(self) -> float:
1✔
109
        return self.amount or self.ft_amount
1✔
110

111
    @property
1✔
112
    def safe_placement_price(self) -> float:
1✔
113
        """Price at which the order was placed"""
114
        return self.price or self.stop_price or self.ft_price
1✔
115

116
    @property
1✔
117
    def safe_price(self) -> float:
1✔
118
        return self.average or self.price or self.stop_price or self.ft_price
1✔
119

120
    @property
1✔
121
    def safe_filled(self) -> float:
1✔
122
        return self.filled if self.filled is not None else 0.0
1✔
123

124
    @property
1✔
125
    def safe_cost(self) -> float:
1✔
126
        return self.cost or 0.0
×
127

128
    @property
1✔
129
    def safe_remaining(self) -> float:
1✔
130
        return (
1✔
131
            self.remaining if self.remaining is not None else
132
            self.safe_amount - (self.filled or 0.0)
133
        )
134

135
    @property
1✔
136
    def safe_fee_base(self) -> float:
1✔
137
        return self.ft_fee_base or 0.0
1✔
138

139
    @property
1✔
140
    def safe_amount_after_fee(self) -> float:
1✔
141
        return self.safe_filled - self.safe_fee_base
1✔
142

143
    @property
1✔
144
    def trade(self) -> "LocalTrade":
1✔
145
        return self._trade_bt or self._trade_live
1✔
146

147
    @property
1✔
148
    def stake_amount(self) -> float:
1✔
149
        """ Amount in stake currency used for this order"""
150
        return self.safe_amount * self.safe_price / self.trade.leverage
1✔
151

152
    def __repr__(self):
1✔
153

154
        return (f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, "
1✔
155
                f"side={self.side}, filled={self.safe_filled}, price={self.safe_price}, "
156
                f"status={self.status}, date={self.order_date_utc:{DATETIME_PRINT_FORMAT}})")
157

158
    def update_from_ccxt_object(self, order):
1✔
159
        """
160
        Update Order from ccxt response
161
        Only updates if fields are available from ccxt -
162
        """
163
        if self.order_id != str(order['id']):
1✔
164
            raise DependencyException("Order-id's don't match")
1✔
165

166
        self.status = safe_value_fallback(order, 'status', default_value=self.status)
1✔
167
        self.symbol = safe_value_fallback(order, 'symbol', default_value=self.symbol)
1✔
168
        self.order_type = safe_value_fallback(order, 'type', default_value=self.order_type)
1✔
169
        self.side = safe_value_fallback(order, 'side', default_value=self.side)
1✔
170
        self.price = safe_value_fallback(order, 'price', default_value=self.price)
1✔
171
        self.amount = safe_value_fallback(order, 'amount', default_value=self.amount)
1✔
172
        self.filled = safe_value_fallback(order, 'filled', default_value=self.filled)
1✔
173
        self.average = safe_value_fallback(order, 'average', default_value=self.average)
1✔
174
        self.remaining = safe_value_fallback(order, 'remaining', default_value=self.remaining)
1✔
175
        self.cost = safe_value_fallback(order, 'cost', default_value=self.cost)
1✔
176
        self.stop_price = safe_value_fallback(order, 'stopPrice', default_value=self.stop_price)
1✔
177
        order_date = safe_value_fallback(order, 'timestamp')
1✔
178
        if order_date:
1✔
179
            self.order_date = datetime.fromtimestamp(order_date / 1000, tz=timezone.utc)
1✔
180
        elif not self.order_date:
1✔
181
            self.order_date = dt_now()
1✔
182

183
        self.ft_is_open = True
1✔
184
        if self.status in NON_OPEN_EXCHANGE_STATES:
1✔
185
            self.ft_is_open = False
1✔
186
            if (order.get('filled', 0.0) or 0.0) > 0 and not self.order_filled_date:
1✔
187
                self.order_filled_date = dt_from_ts(
1✔
188
                    safe_value_fallback(order, 'lastTradeTimestamp', default_value=dt_ts())
189
                )
190
        self.order_update_date = datetime.now(timezone.utc)
1✔
191

192
    def to_ccxt_object(self, stopPriceName: str = 'stopPrice') -> Dict[str, Any]:
1✔
193
        order: Dict[str, Any] = {
1✔
194
            'id': self.order_id,
195
            'symbol': self.ft_pair,
196
            'price': self.price,
197
            'average': self.average,
198
            'amount': self.amount,
199
            'cost': self.cost,
200
            'type': self.order_type,
201
            'side': self.ft_order_side,
202
            'filled': self.filled,
203
            'remaining': self.remaining,
204
            'datetime': self.order_date_utc.strftime('%Y-%m-%dT%H:%M:%S.%f'),
205
            'timestamp': int(self.order_date_utc.timestamp() * 1000),
206
            'status': self.status,
207
            'fee': None,
208
            'info': {},
209
        }
210
        if self.ft_order_side == 'stoploss':
1✔
211
            order.update({
1✔
212
                stopPriceName: self.stop_price,
213
                'ft_order_type': 'stoploss',
214
            })
215

216
        return order
1✔
217

218
    def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]:
1✔
219
        """
220
        :param minified: If True, only return a subset of the data is returned.
221
                         Only used for backtesting.
222
        """
223
        resp = {
1✔
224
            'amount': self.safe_amount,
225
            'safe_price': self.safe_price,
226
            'ft_order_side': self.ft_order_side,
227
            'order_filled_timestamp': dt_ts_none(self.order_filled_utc),
228
            'ft_is_entry': self.ft_order_side == entry_side,
229
            'ft_order_tag': self.ft_order_tag,
230
        }
231
        if not minified:
1✔
232
            resp.update({
1✔
233
                'pair': self.ft_pair,
234
                'order_id': self.order_id,
235
                'status': self.status,
236
                'average': round(self.average, 8) if self.average else 0,
237
                'cost': self.cost if self.cost else 0,
238
                'filled': self.filled,
239
                'is_open': self.ft_is_open,
240
                'order_date': self.order_date.strftime(DATETIME_PRINT_FORMAT)
241
                if self.order_date else None,
242
                'order_timestamp': int(self.order_date.replace(
243
                    tzinfo=timezone.utc).timestamp() * 1000) if self.order_date else None,
244
                'order_filled_date': self.order_filled_date.strftime(DATETIME_PRINT_FORMAT)
245
                if self.order_filled_date else None,
246
                'order_type': self.order_type,
247
                'price': self.price,
248
                'remaining': self.remaining,
249
                'ft_fee_base': self.ft_fee_base,
250
                'funding_fee': self.funding_fee,
251
            })
252
        return resp
1✔
253

254
    def close_bt_order(self, close_date: datetime, trade: 'LocalTrade'):
1✔
255
        self.order_filled_date = close_date
1✔
256
        self.filled = self.amount
1✔
257
        self.remaining = 0
1✔
258
        self.status = 'closed'
1✔
259
        self.ft_is_open = False
1✔
260
        # Assign funding fees to Order.
261
        # Assumes backtesting will use date_last_filled_utc to calculate future funding fees.
262
        self.funding_fee = trade.funding_fee_running
1✔
263
        trade.funding_fee_running = 0.0
1✔
264

265
        if (self.ft_order_side == trade.entry_side and self.price):
1✔
266
            trade.open_rate = self.price
1✔
267
            trade.recalc_trade_from_orders()
1✔
268
            if trade.nr_of_successful_entries == 1:
1✔
269
                trade.initial_stop_loss_pct = None
1✔
270
                trade.is_stop_loss_trailing = False
1✔
271
            trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct)
1✔
272

273
    @staticmethod
1✔
274
    def update_orders(orders: List['Order'], order: Dict[str, Any]):
1✔
275
        """
276
        Get all non-closed orders - useful when trying to batch-update orders
277
        """
278
        if not isinstance(order, dict):
1✔
279
            logger.warning(f"{order} is not a valid response object.")
1✔
280
            return
1✔
281

282
        filtered_orders = [o for o in orders if o.order_id == order.get('id')]
1✔
283
        if filtered_orders:
1✔
284
            oobj = filtered_orders[0]
1✔
285
            oobj.update_from_ccxt_object(order)
1✔
286
            Trade.commit()
1✔
287
        else:
288
            logger.warning(f"Did not find order for {order}.")
1✔
289

290
    @classmethod
1✔
291
    def parse_from_ccxt_object(
1✔
292
            cls, order: Dict[str, Any], pair: str, side: str,
293
            amount: Optional[float] = None, price: Optional[float] = None) -> Self:
294
        """
295
        Parse an order from a ccxt object and return a new order Object.
296
        Optional support for overriding amount and price is only used for test simplification.
297
        """
298
        o = cls(
1✔
299
            order_id=str(order['id']),
300
            ft_order_side=side,
301
            ft_pair=pair,
302
            ft_amount=amount if amount else order['amount'],
303
            ft_price=price if price else order['price'],
304
            )
305

306
        o.update_from_ccxt_object(order)
1✔
307
        return o
1✔
308

309
    @staticmethod
1✔
310
    def get_open_orders() -> Sequence['Order']:
1✔
311
        """
312
        Retrieve open orders from the database
313
        :return: List of open orders
314
        """
315
        return Order.session.scalars(select(Order).filter(Order.ft_is_open.is_(True))).all()
1✔
316

317
    @staticmethod
1✔
318
    def order_by_id(order_id: str) -> Optional['Order']:
1✔
319
        """
320
        Retrieve order based on order_id
321
        :return: Order or None
322
        """
323
        return Order.session.scalars(select(Order).filter(Order.order_id == order_id)).first()
1✔
324

325

326
class LocalTrade:
1✔
327
    """
328
    Trade database model.
329
    Used in backtesting - must be aligned to Trade model!
330

331
    """
332
    use_db: bool = False
1✔
333
    # Trades container for backtesting
334
    trades: List['LocalTrade'] = []
1✔
335
    trades_open: List['LocalTrade'] = []
1✔
336
    # Copy of trades_open - but indexed by pair
337
    bt_trades_open_pp: Dict[str, List['LocalTrade']] = defaultdict(list)
1✔
338
    bt_open_open_trade_count: int = 0
1✔
339
    total_profit: float = 0
1✔
340
    realized_profit: float = 0
1✔
341

342
    id: int = 0
1✔
343

344
    orders: List[Order] = []
1✔
345

346
    exchange: str = ''
1✔
347
    pair: str = ''
1✔
348
    base_currency: Optional[str] = ''
1✔
349
    stake_currency: Optional[str] = ''
1✔
350
    is_open: bool = True
1✔
351
    fee_open: float = 0.0
1✔
352
    fee_open_cost: Optional[float] = None
1✔
353
    fee_open_currency: Optional[str] = ''
1✔
354
    fee_close: Optional[float] = 0.0
1✔
355
    fee_close_cost: Optional[float] = None
1✔
356
    fee_close_currency: Optional[str] = ''
1✔
357
    open_rate: float = 0.0
1✔
358
    open_rate_requested: Optional[float] = None
1✔
359
    # open_trade_value - calculated via _calc_open_trade_value
360
    open_trade_value: float = 0.0
1✔
361
    close_rate: Optional[float] = None
1✔
362
    close_rate_requested: Optional[float] = None
1✔
363
    close_profit: Optional[float] = None
1✔
364
    close_profit_abs: Optional[float] = None
1✔
365
    stake_amount: float = 0.0
1✔
366
    max_stake_amount: Optional[float] = 0.0
1✔
367
    amount: float = 0.0
1✔
368
    amount_requested: Optional[float] = None
1✔
369
    open_date: datetime
1✔
370
    close_date: Optional[datetime] = None
1✔
371
    # absolute value of the stop loss
372
    stop_loss: float = 0.0
1✔
373
    # percentage value of the stop loss
374
    stop_loss_pct: Optional[float] = 0.0
1✔
375
    # absolute value of the initial stop loss
376
    initial_stop_loss: Optional[float] = 0.0
1✔
377
    # percentage value of the initial stop loss
378
    initial_stop_loss_pct: Optional[float] = None
1✔
379
    is_stop_loss_trailing: bool = False
1✔
380
    # absolute value of the highest reached price
381
    max_rate: Optional[float] = None
1✔
382
    # Lowest price reached
383
    min_rate: Optional[float] = None
1✔
384
    exit_reason: Optional[str] = ''
1✔
385
    exit_order_status: Optional[str] = ''
1✔
386
    strategy: Optional[str] = ''
1✔
387
    enter_tag: Optional[str] = None
1✔
388
    timeframe: Optional[int] = None
1✔
389

390
    trading_mode: TradingMode = TradingMode.SPOT
1✔
391
    amount_precision: Optional[float] = None
1✔
392
    price_precision: Optional[float] = None
1✔
393
    precision_mode: Optional[int] = None
1✔
394
    contract_size: Optional[float] = None
1✔
395

396
    # Leverage trading properties
397
    liquidation_price: Optional[float] = None
1✔
398
    is_short: bool = False
1✔
399
    leverage: float = 1.0
1✔
400

401
    # Margin trading properties
402
    interest_rate: float = 0.0
1✔
403

404
    # Futures properties
405
    funding_fees: Optional[float] = None
1✔
406
    # Used to keep running funding fees - between the last filled order and now
407
    # Shall not be used for calculations!
408
    funding_fee_running: Optional[float] = None
1✔
409

410
    @property
1✔
411
    def stoploss_or_liquidation(self) -> float:
1✔
412
        if self.liquidation_price:
1✔
413
            if self.is_short:
1✔
414
                return min(self.stop_loss, self.liquidation_price)
1✔
415
            else:
416
                return max(self.stop_loss, self.liquidation_price)
1✔
417

418
        return self.stop_loss
1✔
419

420
    @property
1✔
421
    def buy_tag(self) -> Optional[str]:
1✔
422
        """
423
        Compatibility between buy_tag (old) and enter_tag (new)
424
        Consider buy_tag deprecated
425
        """
426
        return self.enter_tag
1✔
427

428
    @property
1✔
429
    def has_no_leverage(self) -> bool:
1✔
430
        """Returns true if this is a non-leverage, non-short trade"""
431
        return ((self.leverage == 1.0 or self.leverage is None) and not self.is_short)
1✔
432

433
    @property
1✔
434
    def borrowed(self) -> float:
1✔
435
        """
436
            The amount of currency borrowed from the exchange for leverage trades
437
            If a long trade, the amount is in base currency
438
            If a short trade, the amount is in the other currency being traded
439
        """
440
        if self.has_no_leverage:
1✔
441
            return 0.0
1✔
442
        elif not self.is_short:
1✔
443
            return (self.amount * self.open_rate) * ((self.leverage - 1) / self.leverage)
1✔
444
        else:
445
            return self.amount
1✔
446

447
    @property
1✔
448
    def _date_last_filled_utc(self) -> Optional[datetime]:
1✔
449
        """ Date of the last filled order"""
450
        orders = self.select_filled_orders()
1✔
451
        if orders:
1✔
452
            return max(o.order_filled_utc for o in orders if o.order_filled_utc)
1✔
453
        return None
1✔
454

455
    @property
1✔
456
    def date_last_filled_utc(self) -> datetime:
1✔
457
        """ Date of the last filled order - or open_date if no orders are filled"""
458
        dt_last_filled = self._date_last_filled_utc
1✔
459
        if not dt_last_filled:
1✔
460
            return self.open_date_utc
1✔
461
        return max([self.open_date_utc, dt_last_filled])
1✔
462

463
    @property
1✔
464
    def date_entry_fill_utc(self) -> Optional[datetime]:
1✔
465
        """ Date of the first filled order"""
466
        orders = self.select_filled_orders(self.entry_side)
1✔
467
        if (
1✔
468
            orders
469
            and len(filled_date := [o.order_filled_utc for o in orders if o.order_filled_utc])
470
        ):
471
            return min(filled_date)
1✔
472
        return None
1✔
473

474
    @property
1✔
475
    def open_date_utc(self):
1✔
476
        return self.open_date.replace(tzinfo=timezone.utc)
1✔
477

478
    @property
1✔
479
    def stoploss_last_update_utc(self):
1✔
480
        if self.has_open_sl_orders:
1✔
481
            return max(o.order_date_utc for o in self.open_sl_orders)
1✔
482
        return None
1✔
483

484
    @property
1✔
485
    def close_date_utc(self):
1✔
486
        return self.close_date.replace(tzinfo=timezone.utc) if self.close_date else None
1✔
487

488
    @property
1✔
489
    def entry_side(self) -> str:
1✔
490
        if self.is_short:
1✔
491
            return "sell"
1✔
492
        else:
493
            return "buy"
1✔
494

495
    @property
1✔
496
    def exit_side(self) -> BuySell:
1✔
497
        if self.is_short:
1✔
498
            return "buy"
1✔
499
        else:
500
            return "sell"
1✔
501

502
    @property
1✔
503
    def trade_direction(self) -> LongShort:
1✔
504
        if self.is_short:
1✔
505
            return "short"
1✔
506
        else:
507
            return "long"
1✔
508

509
    @property
1✔
510
    def safe_base_currency(self) -> str:
1✔
511
        """
512
        Compatibility layer for asset - which can be empty for old trades.
513
        """
514
        try:
1✔
515
            return self.base_currency or self.pair.split('/')[0]
1✔
516
        except IndexError:
×
517
            return ''
×
518

519
    @property
1✔
520
    def safe_quote_currency(self) -> str:
1✔
521
        """
522
        Compatibility layer for asset - which can be empty for old trades.
523
        """
524
        try:
1✔
525
            return self.stake_currency or self.pair.split('/')[1].split(':')[0]
1✔
526
        except IndexError:
×
527
            return ''
×
528

529
    @property
1✔
530
    def open_orders(self) -> List[Order]:
1✔
531
        """
532
        All open orders for this trade excluding stoploss orders
533
        """
534
        return [o for o in self.orders if o.ft_is_open and o.ft_order_side != 'stoploss']
1✔
535

536
    @property
1✔
537
    def has_open_orders(self) -> bool:
1✔
538
        """
539
        True if there are open orders for this trade excluding stoploss orders
540
        """
541
        open_orders_wo_sl = [
1✔
542
            o for o in self.orders
543
            if o.ft_order_side not in ['stoploss'] and o.ft_is_open
544
        ]
545
        return len(open_orders_wo_sl) > 0
1✔
546

547
    @property
1✔
548
    def open_sl_orders(self) -> List[Order]:
1✔
549
        """
550
        All open stoploss orders for this trade
551
        """
552
        return [
1✔
553
            o for o in self.orders
554
            if o.ft_order_side in ['stoploss'] and o.ft_is_open
555
        ]
556

557
    @property
1✔
558
    def has_open_sl_orders(self) -> bool:
1✔
559
        """
560
        True if there are open stoploss orders for this trade
561
        """
562
        open_sl_orders = [
1✔
563
            o for o in self.orders
564
            if o.ft_order_side in ['stoploss'] and o.ft_is_open
565
        ]
566
        return len(open_sl_orders) > 0
1✔
567

568
    @property
1✔
569
    def sl_orders(self) -> List[Order]:
1✔
570
        """
571
        All stoploss orders for this trade
572
        """
573
        return [
×
574
            o for o in self.orders
575
            if o.ft_order_side in ['stoploss']
576
        ]
577

578
    @property
1✔
579
    def open_orders_ids(self) -> List[str]:
1✔
580
        open_orders_ids_wo_sl = [
1✔
581
            oo.order_id for oo in self.open_orders
582
            if oo.ft_order_side not in ['stoploss']
583
        ]
584
        return open_orders_ids_wo_sl
1✔
585

586
    def __init__(self, **kwargs):
1✔
587
        for key in kwargs:
1✔
588
            setattr(self, key, kwargs[key])
1✔
589
        self.recalc_open_trade_value()
1✔
590
        self.orders = []
1✔
591
        if self.trading_mode == TradingMode.MARGIN and self.interest_rate is None:
1✔
592
            raise OperationalException(
×
593
                f"{self.trading_mode.value} trading requires param interest_rate on trades")
594

595
    def __repr__(self):
1✔
596
        open_since = (
1✔
597
            self.open_date_utc.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
598
        )
599

600
        return (
1✔
601
            f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
602
            f'is_short={self.is_short or False}, leverage={self.leverage or 1.0}, '
603
            f'open_rate={self.open_rate:.8f}, open_since={open_since})'
604
        )
605

606
    def to_json(self, minified: bool = False) -> Dict[str, Any]:
1✔
607
        """
608
        :param minified: If True, only return a subset of the data is returned.
609
                         Only used for backtesting.
610
        :return: Dictionary with trade data
611
        """
612
        filled_or_open_orders = self.select_filled_or_open_orders()
1✔
613
        orders_json = [order.to_json(self.entry_side, minified) for order in filled_or_open_orders]
1✔
614

615
        return {
1✔
616
            'trade_id': self.id,
617
            'pair': self.pair,
618
            'base_currency': self.safe_base_currency,
619
            'quote_currency': self.safe_quote_currency,
620
            'is_open': self.is_open,
621
            'exchange': self.exchange,
622
            'amount': round(self.amount, 8),
623
            'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
624
            'stake_amount': round(self.stake_amount, 8),
625
            'max_stake_amount': round(self.max_stake_amount, 8) if self.max_stake_amount else None,
626
            'strategy': self.strategy,
627
            'enter_tag': self.enter_tag,
628
            'timeframe': self.timeframe,
629

630
            'fee_open': self.fee_open,
631
            'fee_open_cost': self.fee_open_cost,
632
            'fee_open_currency': self.fee_open_currency,
633
            'fee_close': self.fee_close,
634
            'fee_close_cost': self.fee_close_cost,
635
            'fee_close_currency': self.fee_close_currency,
636

637
            'open_date': self.open_date.strftime(DATETIME_PRINT_FORMAT),
638
            'open_timestamp': dt_ts_none(self.open_date_utc),
639
            'open_fill_date': (self.date_entry_fill_utc.strftime(DATETIME_PRINT_FORMAT)
640
                               if self.date_entry_fill_utc else None),
641
            'open_fill_timestamp': dt_ts_none(self.date_entry_fill_utc),
642
            'open_rate': self.open_rate,
643
            'open_rate_requested': self.open_rate_requested,
644
            'open_trade_value': round(self.open_trade_value, 8),
645

646
            'close_date': (self.close_date.strftime(DATETIME_PRINT_FORMAT)
647
                           if self.close_date else None),
648
            'close_timestamp': dt_ts_none(self.close_date_utc),
649
            'realized_profit': self.realized_profit or 0.0,
650
            # Close-profit corresponds to relative realized_profit ratio
651
            'realized_profit_ratio': self.close_profit or None,
652
            'close_rate': self.close_rate,
653
            'close_rate_requested': self.close_rate_requested,
654
            'close_profit': self.close_profit,  # Deprecated
655
            'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
656
            'close_profit_abs': self.close_profit_abs,  # Deprecated
657

658
            'trade_duration_s': (int((self.close_date_utc - self.open_date_utc).total_seconds())
659
                                 if self.close_date else None),
660
            'trade_duration': (int((self.close_date_utc - self.open_date_utc).total_seconds() // 60)
661
                               if self.close_date else None),
662

663
            'profit_ratio': self.close_profit,
664
            'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
665
            'profit_abs': self.close_profit_abs,
666

667
            'exit_reason': self.exit_reason,
668
            'exit_order_status': self.exit_order_status,
669
            'stop_loss_abs': self.stop_loss,
670
            'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
671
            'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
672
            'stoploss_last_update': (self.stoploss_last_update_utc.strftime(DATETIME_PRINT_FORMAT)
673
                                     if self.stoploss_last_update_utc else None),
674
            'stoploss_last_update_timestamp': dt_ts_none(self.stoploss_last_update_utc),
675
            'initial_stop_loss_abs': self.initial_stop_loss,
676
            'initial_stop_loss_ratio': (self.initial_stop_loss_pct
677
                                        if self.initial_stop_loss_pct else None),
678
            'initial_stop_loss_pct': (self.initial_stop_loss_pct * 100
679
                                      if self.initial_stop_loss_pct else None),
680
            'min_rate': self.min_rate,
681
            'max_rate': self.max_rate,
682

683
            'leverage': self.leverage,
684
            'interest_rate': self.interest_rate,
685
            'liquidation_price': self.liquidation_price,
686
            'is_short': self.is_short,
687
            'trading_mode': self.trading_mode,
688
            'funding_fees': self.funding_fees,
689
            'amount_precision': self.amount_precision,
690
            'price_precision': self.price_precision,
691
            'precision_mode': self.precision_mode,
692
            'contract_size': self.contract_size,
693
            'has_open_orders': self.has_open_orders,
694
            'orders': orders_json,
695
        }
696

697
    @staticmethod
1✔
698
    def reset_trades() -> None:
1✔
699
        """
700
        Resets all trades. Only active for backtesting mode.
701
        """
702
        LocalTrade.trades = []
1✔
703
        LocalTrade.trades_open = []
1✔
704
        LocalTrade.bt_trades_open_pp = defaultdict(list)
1✔
705
        LocalTrade.bt_open_open_trade_count = 0
1✔
706
        LocalTrade.total_profit = 0
1✔
707

708
    def adjust_min_max_rates(self, current_price: float, current_price_low: float) -> None:
1✔
709
        """
710
        Adjust the max_rate and min_rate.
711
        """
712
        self.max_rate = max(current_price, self.max_rate or self.open_rate)
1✔
713
        self.min_rate = min(current_price_low, self.min_rate or self.open_rate)
1✔
714

715
    def set_liquidation_price(self, liquidation_price: Optional[float]):
1✔
716
        """
717
        Method you should use to set self.liquidation price.
718
        Assures stop_loss is not passed the liquidation price
719
        """
720
        if not liquidation_price:
1✔
721
            return
1✔
722
        self.liquidation_price = liquidation_price
1✔
723

724
    def set_funding_fees(self, funding_fee: float) -> None:
1✔
725
        """
726
        Assign funding fees to Trade.
727
        """
728
        if funding_fee is None:
1✔
729
            return
×
730
        self.funding_fee_running = funding_fee
1✔
731
        prior_funding_fees = sum([o.funding_fee for o in self.orders if o.funding_fee])
1✔
732
        self.funding_fees = prior_funding_fees + funding_fee
1✔
733

734
    def __set_stop_loss(self, stop_loss: float, percent: float):
1✔
735
        """
736
        Method used internally to set self.stop_loss.
737
        """
738
        if not self.stop_loss:
1✔
739
            self.initial_stop_loss = stop_loss
1✔
740
        self.stop_loss = stop_loss
1✔
741

742
        self.stop_loss_pct = -1 * abs(percent)
1✔
743

744
    def adjust_stop_loss(self, current_price: float, stoploss: Optional[float],
1✔
745
                         initial: bool = False, allow_refresh: bool = False) -> None:
746
        """
747
        This adjusts the stop loss to it's most recently observed setting
748
        :param current_price: Current rate the asset is traded
749
        :param stoploss: Stoploss as factor (sample -0.05 -> -5% below current price).
750
        :param initial: Called to initiate stop_loss.
751
            Skips everything if self.stop_loss is already set.
752
        :param refresh: Called to refresh stop_loss, allows adjustment in both directions
753
        """
754
        if stoploss is None or (initial and not (self.stop_loss is None or self.stop_loss == 0)):
1✔
755
            # Don't modify if called with initial and nothing to do
756
            return
1✔
757

758
        leverage = self.leverage or 1.0
1✔
759
        if self.is_short:
1✔
760
            new_loss = float(current_price * (1 + abs(stoploss / leverage)))
1✔
761
        else:
762
            new_loss = float(current_price * (1 - abs(stoploss / leverage)))
1✔
763

764
        stop_loss_norm = price_to_precision(new_loss, self.price_precision, self.precision_mode,
1✔
765
                                            rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP)
766
        # no stop loss assigned yet
767
        if self.initial_stop_loss_pct is None:
1✔
768
            self.__set_stop_loss(stop_loss_norm, stoploss)
1✔
769
            self.initial_stop_loss = price_to_precision(
1✔
770
                stop_loss_norm, self.price_precision, self.precision_mode,
771
                rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP)
772
            self.initial_stop_loss_pct = -1 * abs(stoploss)
1✔
773

774
        # evaluate if the stop loss needs to be updated
775
        else:
776
            higher_stop = stop_loss_norm > self.stop_loss
1✔
777
            lower_stop = stop_loss_norm < self.stop_loss
1✔
778

779
            # stop losses only walk up, never down!,
780
            #   ? But adding more to a leveraged trade would create a lower liquidation price,
781
            #   ? decreasing the minimum stoploss
782
            if (
1✔
783
                allow_refresh
784
                or (higher_stop and not self.is_short)
785
                or (lower_stop and self.is_short)
786
            ):
787
                logger.debug(f"{self.pair} - Adjusting stoploss...")
1✔
788
                if not allow_refresh:
1✔
789
                    self.is_stop_loss_trailing = True
1✔
790
                self.__set_stop_loss(stop_loss_norm, stoploss)
1✔
791
            else:
792
                logger.debug(f"{self.pair} - Keeping current stoploss...")
1✔
793

794
        logger.debug(
1✔
795
            f"{self.pair} - Stoploss adjusted. current_price={current_price:.8f}, "
796
            f"open_rate={self.open_rate:.8f}, max_rate={self.max_rate or self.open_rate:.8f}, "
797
            f"initial_stop_loss={self.initial_stop_loss:.8f}, "
798
            f"stop_loss={self.stop_loss:.8f}. "
799
            f"Trailing stoploss saved us: "
800
            f"{float(self.stop_loss) - float(self.initial_stop_loss or 0.0):.8f}.")
801

802
    def update_trade(self, order: Order, recalculating: bool = False) -> None:
1✔
803
        """
804
        Updates this entity with amount and actual open/close rates.
805
        :param order: order retrieved by exchange.fetch_order()
806
        :return: None
807
        """
808

809
        # Ignore open and cancelled orders
810
        if order.status == 'open' or order.safe_price is None:
1✔
811
            return
1✔
812

813
        logger.info(f'Updating trade (id={self.id}) ...')
1✔
814
        if order.ft_order_side != 'stoploss':
1✔
815
            order.funding_fee = self.funding_fee_running
1✔
816
            # Reset running funding fees
817
            self.funding_fee_running = 0.0
1✔
818
        order_type = order.order_type.upper() if order.order_type else None
1✔
819

820
        if order.ft_order_side == self.entry_side:
1✔
821
            # Update open rate and actual amount
822
            self.open_rate = order.safe_price
1✔
823
            self.amount = order.safe_amount_after_fee
1✔
824
            if self.is_open:
1✔
825
                payment = "SELL" if self.is_short else "BUY"
1✔
826
                logger.info(f'{order_type}_{payment} has been fulfilled for {self}.')
1✔
827

828
            self.recalc_trade_from_orders()
1✔
829
        elif order.ft_order_side == self.exit_side:
1✔
830
            if self.is_open:
1✔
831
                payment = "BUY" if self.is_short else "SELL"
1✔
832
                # * On margin shorts, you buy a little bit more than the amount (amount + interest)
833
                logger.info(f'{order_type}_{payment} has been fulfilled for {self}.')
1✔
834

835
        elif order.ft_order_side == 'stoploss' and order.status not in ('open', ):
1✔
836
            self.close_rate_requested = self.stop_loss
1✔
837
            self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
1✔
838
            if self.is_open and order.safe_filled > 0:
1✔
839
                logger.info(f'{order_type} is hit for {self}.')
1✔
840
        else:
841
            raise ValueError(f'Unknown order type: {order.order_type}')
1✔
842

843
        if order.ft_order_side != self.entry_side:
1✔
844
            amount_tr = amount_to_contract_precision(self.amount, self.amount_precision,
1✔
845
                                                     self.precision_mode, self.contract_size)
846
            if (
1✔
847
                isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC)
848
                or (not recalculating and order.safe_amount_after_fee > amount_tr)
849
            ):
850
                # When recalculating a trade, only coming out to 0 can force a close
851
                self.close(order.safe_price)
1✔
852
            else:
853
                self.recalc_trade_from_orders()
1✔
854

855
        Trade.commit()
1✔
856

857
    def close(self, rate: float, *, show_msg: bool = True) -> None:
1✔
858
        """
859
        Sets close_rate to the given rate, calculates total profit
860
        and marks trade as closed
861
        """
862
        self.close_rate = rate
1✔
863
        self.close_date = self.close_date or self._date_last_filled_utc or dt_now()
1✔
864
        self.is_open = False
1✔
865
        self.exit_order_status = 'closed'
1✔
866
        self.recalc_trade_from_orders(is_closing=True)
1✔
867
        if show_msg:
1✔
868
            logger.info(f"Marking {self} as closed as the trade is fulfilled "
1✔
869
                        "and found no open orders for it.")
870

871
    def update_fee(self, fee_cost: float, fee_currency: Optional[str], fee_rate: Optional[float],
1✔
872
                   side: str) -> None:
873
        """
874
        Update Fee parameters. Only acts once per side
875
        """
876
        if self.entry_side == side and self.fee_open_currency is None:
1✔
877
            self.fee_open_cost = fee_cost
1✔
878
            self.fee_open_currency = fee_currency
1✔
879
            if fee_rate is not None:
1✔
880
                self.fee_open = fee_rate
1✔
881
                # Assume close-fee will fall into the same fee category and take an educated guess
882
                self.fee_close = fee_rate
1✔
883
        elif self.exit_side == side and self.fee_close_currency is None:
1✔
884
            self.fee_close_cost = fee_cost
1✔
885
            self.fee_close_currency = fee_currency
1✔
886
            if fee_rate is not None:
1✔
887
                self.fee_close = fee_rate
1✔
888

889
    def fee_updated(self, side: str) -> bool:
1✔
890
        """
891
        Verify if this side (buy / sell) has already been updated
892
        """
893
        if self.entry_side == side:
1✔
894
            return self.fee_open_currency is not None
1✔
895
        elif self.exit_side == side:
1✔
896
            return self.fee_close_currency is not None
1✔
897
        else:
898
            return False
1✔
899

900
    def update_order(self, order: Dict) -> None:
1✔
901
        Order.update_orders(self.orders, order)
1✔
902

903
    def get_canceled_exit_order_count(self) -> int:
1✔
904
        """
905
        Get amount of failed exiting orders
906
        assumes full exits.
907
        """
908
        return len([o for o in self.orders if o.ft_order_side == self.exit_side
1✔
909
                    and o.status in CANCELED_EXCHANGE_STATES])
910

911
    def _calc_open_trade_value(self, amount: float, open_rate: float) -> float:
1✔
912
        """
913
        Calculate the open_rate including open_fee.
914
        :return: Price in of the open trade incl. Fees
915
        """
916
        open_trade = FtPrecise(amount) * FtPrecise(open_rate)
1✔
917
        fees = open_trade * FtPrecise(self.fee_open)
1✔
918
        if self.is_short:
1✔
919
            return float(open_trade - fees)
1✔
920
        else:
921
            return float(open_trade + fees)
1✔
922

923
    def recalc_open_trade_value(self) -> None:
1✔
924
        """
925
        Recalculate open_trade_value.
926
        Must be called whenever open_rate, fee_open is changed.
927
        """
928
        self.open_trade_value = self._calc_open_trade_value(self.amount, self.open_rate)
1✔
929

930
    def calculate_interest(self) -> FtPrecise:
1✔
931
        """
932
        Calculate interest for this trade. Only applicable for Margin trading.
933
        """
934
        zero = FtPrecise(0.0)
1✔
935
        # If nothing was borrowed
936
        if self.trading_mode != TradingMode.MARGIN or self.has_no_leverage:
1✔
937
            return zero
1✔
938

939
        open_date = self.open_date.replace(tzinfo=None)
1✔
940
        now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None)
1✔
941
        sec_per_hour = FtPrecise(3600)
1✔
942
        total_seconds = FtPrecise((now - open_date).total_seconds())
1✔
943
        hours = total_seconds / sec_per_hour or zero
1✔
944

945
        rate = FtPrecise(self.interest_rate)
1✔
946
        borrowed = FtPrecise(self.borrowed)
1✔
947

948
        return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
1✔
949

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

952
        close_trade = amount * FtPrecise(rate)
1✔
953
        fees = close_trade * FtPrecise(fee or 0.0)
1✔
954

955
        if self.is_short:
1✔
956
            return close_trade + fees
1✔
957
        else:
958
            return close_trade - fees
1✔
959

960
    def calc_close_trade_value(self, rate: float, amount: Optional[float] = None) -> float:
1✔
961
        """
962
        Calculate the Trade's close value including fees
963
        :param rate: rate to compare with.
964
        :return: value in stake currency of the open trade
965
        """
966
        if rate is None and not self.close_rate:
1✔
967
            return 0.0
1✔
968

969
        amount1 = FtPrecise(amount or self.amount)
1✔
970
        trading_mode = self.trading_mode or TradingMode.SPOT
1✔
971

972
        if trading_mode == TradingMode.SPOT:
1✔
973
            return float(self._calc_base_close(amount1, rate, self.fee_close))
1✔
974

975
        elif (trading_mode == TradingMode.MARGIN):
1✔
976

977
            total_interest = self.calculate_interest()
1✔
978

979
            if self.is_short:
1✔
980
                amount1 = amount1 + total_interest
1✔
981
                return float(self._calc_base_close(amount1, rate, self.fee_close))
1✔
982
            else:
983
                # Currency already owned for longs, no need to purchase
984
                return float(self._calc_base_close(amount1, rate, self.fee_close) - total_interest)
1✔
985

986
        elif (trading_mode == TradingMode.FUTURES):
1✔
987
            funding_fees = self.funding_fees or 0.0
1✔
988
            # Positive funding_fees -> Trade has gained from fees.
989
            # Negative funding_fees -> Trade had to pay the fees.
990
            if self.is_short:
1✔
991
                return float(self._calc_base_close(amount1, rate, self.fee_close)) - funding_fees
1✔
992
            else:
993
                return float(self._calc_base_close(amount1, rate, self.fee_close)) + funding_fees
1✔
994
        else:
995
            raise OperationalException(
×
996
                f"{self.trading_mode.value} trading is not yet available using freqtrade")
997

998
    def calc_profit(self, rate: float, amount: Optional[float] = None,
1✔
999
                    open_rate: Optional[float] = None) -> float:
1000
        """
1001
        Calculate the absolute profit in stake currency between Close and Open trade
1002
        Deprecated - only available for backwards compatibility
1003
        :param rate: close rate to compare with.
1004
        :param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
1005
        :param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
1006
        :return: profit in stake currency as float
1007
        """
1008
        prof = self.calculate_profit(rate, amount, open_rate)
1✔
1009
        return prof.profit_abs
1✔
1010

1011
    def calculate_profit(self, rate: float, amount: Optional[float] = None,
1✔
1012
                         open_rate: Optional[float] = None) -> ProfitStruct:
1013
        """
1014
        Calculate profit metrics (absolute, ratio, total, total ratio).
1015
        All calculations include fees.
1016
        :param rate: close rate to compare with.
1017
        :param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
1018
        :param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
1019
        :return: Profit structure, containing absolute and relative profits.
1020
        """
1021

1022
        close_trade_value = self.calc_close_trade_value(rate, amount)
1✔
1023
        if amount is None or open_rate is None:
1✔
1024
            open_trade_value = self.open_trade_value
1✔
1025
        else:
1026
            open_trade_value = self._calc_open_trade_value(amount, open_rate)
1✔
1027

1028
        if self.is_short:
1✔
1029
            profit_abs = open_trade_value - close_trade_value
1✔
1030
        else:
1031
            profit_abs = close_trade_value - open_trade_value
1✔
1032

1033
        try:
1✔
1034
            if self.is_short:
1✔
1035
                profit_ratio = (1 - (close_trade_value / open_trade_value)) * self.leverage
1✔
1036
            else:
1037
                profit_ratio = ((close_trade_value / open_trade_value) - 1) * self.leverage
1✔
1038
            profit_ratio = float(f"{profit_ratio:.8f}")
1✔
1039
        except ZeroDivisionError:
×
1040
            profit_ratio = 0.0
×
1041

1042
        total_profit_abs = profit_abs + self.realized_profit
1✔
1043
        total_profit_ratio = (
1✔
1044
            (total_profit_abs / self.max_stake_amount) * self.leverage
1045
            if self.max_stake_amount else 0.0
1046
        )
1047
        total_profit_ratio = float(f"{total_profit_ratio:.8f}")
1✔
1048
        profit_abs = float(f"{profit_abs:.8f}")
1✔
1049

1050
        return ProfitStruct(
1✔
1051
            profit_abs=profit_abs,
1052
            profit_ratio=profit_ratio,
1053
            total_profit=profit_abs + self.realized_profit,
1054
            total_profit_ratio=total_profit_ratio,
1055
        )
1056

1057
    def calc_profit_ratio(
1✔
1058
            self, rate: float, amount: Optional[float] = None,
1059
            open_rate: Optional[float] = None) -> float:
1060
        """
1061
        Calculates the profit as ratio (including fee).
1062
        :param rate: rate to compare with.
1063
        :param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
1064
        :param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
1065
        :return: profit ratio as float
1066
        """
1067
        close_trade_value = self.calc_close_trade_value(rate, amount)
1✔
1068

1069
        if amount is None or open_rate is None:
1✔
1070
            open_trade_value = self.open_trade_value
1✔
1071
        else:
1072
            open_trade_value = self._calc_open_trade_value(amount, open_rate)
1✔
1073

1074
        short_close_zero = (self.is_short and close_trade_value == 0.0)
1✔
1075
        long_close_zero = (not self.is_short and open_trade_value == 0.0)
1✔
1076

1077
        if (short_close_zero or long_close_zero):
1✔
1078
            return 0.0
×
1079
        else:
1080
            if self.is_short:
1✔
1081
                profit_ratio = (1 - (close_trade_value / open_trade_value)) * self.leverage
1✔
1082
            else:
1083
                profit_ratio = ((close_trade_value / open_trade_value) - 1) * self.leverage
1✔
1084

1085
        return float(f"{profit_ratio:.8f}")
1✔
1086

1087
    def recalc_trade_from_orders(self, *, is_closing: bool = False):
1✔
1088
        ZERO = FtPrecise(0.0)
1✔
1089
        current_amount = FtPrecise(0.0)
1✔
1090
        current_stake = FtPrecise(0.0)
1✔
1091
        max_stake_amount = FtPrecise(0.0)
1✔
1092
        total_stake = 0.0  # Total stake after all buy orders (does not subtract!)
1✔
1093
        avg_price = FtPrecise(0.0)
1✔
1094
        close_profit = 0.0
1✔
1095
        close_profit_abs = 0.0
1✔
1096
        # Reset funding fees
1097
        self.funding_fees = 0.0
1✔
1098
        funding_fees = 0.0
1✔
1099
        ordercount = len(self.orders) - 1
1✔
1100
        for i, o in enumerate(self.orders):
1✔
1101
            if o.ft_is_open or not o.filled:
1✔
1102
                continue
1✔
1103
            funding_fees += (o.funding_fee or 0.0)
1✔
1104
            tmp_amount = FtPrecise(o.safe_amount_after_fee)
1✔
1105
            tmp_price = FtPrecise(o.safe_price)
1✔
1106

1107
            is_exit = o.ft_order_side != self.entry_side
1✔
1108
            side = FtPrecise(-1 if is_exit else 1)
1✔
1109
            if tmp_amount > ZERO and tmp_price is not None:
1✔
1110
                current_amount += tmp_amount * side
1✔
1111
                price = avg_price if is_exit else tmp_price
1✔
1112
                current_stake += price * tmp_amount * side
1✔
1113

1114
                if current_amount > ZERO and not is_exit:
1✔
1115
                    avg_price = current_stake / current_amount
1✔
1116

1117
            if is_exit:
1✔
1118
                # Process exits
1119
                if i == ordercount and is_closing:
1✔
1120
                    # Apply funding fees only to the last closing order
1121
                    self.funding_fees = funding_fees
1✔
1122

1123
                exit_rate = o.safe_price
1✔
1124
                exit_amount = o.safe_amount_after_fee
1✔
1125
                prof = self.calculate_profit(exit_rate, exit_amount, float(avg_price))
1✔
1126
                close_profit_abs += prof.profit_abs
1✔
1127
                if total_stake > 0:
1✔
1128
                    # This needs to be calculated based on the last occurring exit to be aligned
1129
                    # with realized_profit.
1130
                    close_profit = (close_profit_abs / total_stake) * self.leverage
1✔
1131
            else:
1132
                total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price)
1✔
1133
                max_stake_amount += (tmp_amount * price)
1✔
1134
        self.funding_fees = funding_fees
1✔
1135
        self.max_stake_amount = float(max_stake_amount)
1✔
1136

1137
        if close_profit:
1✔
1138
            self.close_profit = close_profit
1✔
1139
            self.realized_profit = close_profit_abs
1✔
1140
            self.close_profit_abs = prof.profit_abs
1✔
1141

1142
        current_amount_tr = amount_to_contract_precision(
1✔
1143
            float(current_amount), self.amount_precision, self.precision_mode, self.contract_size)
1144
        if current_amount_tr > 0.0:
1✔
1145
            # Trade is still open
1146
            # Leverage not updated, as we don't allow changing leverage through DCA at the moment.
1147
            self.open_rate = float(current_stake / current_amount)
1✔
1148
            self.amount = current_amount_tr
1✔
1149
            self.stake_amount = float(current_stake) / (self.leverage or 1.0)
1✔
1150
            self.fee_open_cost = self.fee_open * float(current_stake)
1✔
1151
            self.recalc_open_trade_value()
1✔
1152
            if self.stop_loss_pct is not None and self.open_rate is not None:
1✔
1153
                self.adjust_stop_loss(self.open_rate, self.stop_loss_pct)
1✔
1154
        elif is_closing and total_stake > 0:
1✔
1155
            # Close profit abs / maximum owned
1156
            # Fees are considered as they are part of close_profit_abs
1157
            self.close_profit = (close_profit_abs / total_stake) * self.leverage
1✔
1158
            self.close_profit_abs = close_profit_abs
1✔
1159

1160
    def select_order_by_order_id(self, order_id: str) -> Optional[Order]:
1✔
1161
        """
1162
        Finds order object by Order id.
1163
        :param order_id: Exchange order id
1164
        """
1165
        for o in self.orders:
1✔
1166
            if o.order_id == order_id:
1✔
1167
                return o
1✔
1168
        return None
×
1169

1170
    def select_order(self, order_side: Optional[str] = None,
1✔
1171
                     is_open: Optional[bool] = None, only_filled: bool = False) -> Optional[Order]:
1172
        """
1173
        Finds latest order for this orderside and status
1174
        :param order_side: ft_order_side of the order (either 'buy', 'sell' or 'stoploss')
1175
        :param is_open: Only search for open orders?
1176
        :param only_filled: Only search for Filled orders (only valid with is_open=False).
1177
        :return: latest Order object if it exists, else None
1178
        """
1179
        orders = self.orders
1✔
1180
        if order_side:
1✔
1181
            orders = [o for o in orders if o.ft_order_side == order_side]
1✔
1182
        if is_open is not None:
1✔
1183
            orders = [o for o in orders if o.ft_is_open == is_open]
1✔
1184
        if is_open is False and only_filled:
1✔
1185
            orders = [o for o in orders if o.filled and o.status in NON_OPEN_EXCHANGE_STATES]
1✔
1186
        if len(orders) > 0:
1✔
1187
            return orders[-1]
1✔
1188
        else:
1189
            return None
1✔
1190

1191
    def select_filled_orders(self, order_side: Optional[str] = None) -> List['Order']:
1✔
1192
        """
1193
        Finds filled orders for this order side.
1194
        Will not return open orders which already partially filled.
1195
        :param order_side: Side of the order (either 'buy', 'sell', or None)
1196
        :return: array of Order objects
1197
        """
1198
        return [o for o in self.orders if ((o.ft_order_side == order_side) or (order_side is None))
1✔
1199
                and o.ft_is_open is False
1200
                and o.filled
1201
                and o.status in NON_OPEN_EXCHANGE_STATES]
1202

1203
    def select_filled_or_open_orders(self) -> List['Order']:
1✔
1204
        """
1205
        Finds filled or open orders
1206
        :param order_side: Side of the order (either 'buy', 'sell', or None)
1207
        :return: array of Order objects
1208
        """
1209
        return [o for o in self.orders if
1✔
1210
                (
1211
                    o.ft_is_open is False
1212
                    and (o.filled or 0) > 0
1213
                    and o.status in NON_OPEN_EXCHANGE_STATES
1214
                    )
1215
                or (o.ft_is_open is True and o.status is not None)
1216
                ]
1217

1218
    def set_custom_data(self, key: str, value: Any) -> None:
1✔
1219
        """
1220
        Set custom data for this trade
1221
        :param key: key of the custom data
1222
        :param value: value of the custom data (must be JSON serializable)
1223
        """
1224
        CustomDataWrapper.set_custom_data(trade_id=self.id, key=key, value=value)
1✔
1225

1226
    def get_custom_data(self, key: str, default: Any = None) -> Any:
1✔
1227
        """
1228
        Get custom data for this trade
1229
        :param key: key of the custom data
1230
        """
1231
        data = CustomDataWrapper.get_custom_data(trade_id=self.id, key=key)
1✔
1232
        if data:
1✔
1233
            return data[0].value
1✔
1234
        return default
1✔
1235

1236
    def get_custom_data_entry(self, key: str) -> Optional[_CustomData]:
1✔
1237
        """
1238
        Get custom data for this trade
1239
        :param key: key of the custom data
1240
        """
1241
        data = CustomDataWrapper.get_custom_data(trade_id=self.id, key=key)
×
1242
        if data:
×
1243
            return data[0]
×
1244
        return None
×
1245

1246
    def get_all_custom_data(self) -> List[_CustomData]:
1✔
1247
        """
1248
        Get all custom data for this trade
1249
        """
1250
        return CustomDataWrapper.get_custom_data(trade_id=self.id)
1✔
1251

1252
    @property
1✔
1253
    def nr_of_successful_entries(self) -> int:
1✔
1254
        """
1255
        Helper function to count the number of entry orders that have been filled.
1256
        :return: int count of entry orders that have been filled for this trade.
1257
        """
1258

1259
        return len(self.select_filled_orders(self.entry_side))
1✔
1260

1261
    @property
1✔
1262
    def nr_of_successful_exits(self) -> int:
1✔
1263
        """
1264
        Helper function to count the number of exit orders that have been filled.
1265
        :return: int count of exit orders that have been filled for this trade.
1266
        """
1267
        return len(self.select_filled_orders(self.exit_side))
1✔
1268

1269
    @property
1✔
1270
    def nr_of_successful_buys(self) -> int:
1✔
1271
        """
1272
        Helper function to count the number of buy orders that have been filled.
1273
        WARNING: Please use nr_of_successful_entries for short support.
1274
        :return: int count of buy orders that have been filled for this trade.
1275
        """
1276

1277
        return len(self.select_filled_orders('buy'))
1✔
1278

1279
    @property
1✔
1280
    def nr_of_successful_sells(self) -> int:
1✔
1281
        """
1282
        Helper function to count the number of sell orders that have been filled.
1283
        WARNING: Please use nr_of_successful_exits for short support.
1284
        :return: int count of sell orders that have been filled for this trade.
1285
        """
1286
        return len(self.select_filled_orders('sell'))
×
1287

1288
    @property
1✔
1289
    def sell_reason(self) -> Optional[str]:
1✔
1290
        """ DEPRECATED! Please use exit_reason instead."""
1291
        return self.exit_reason
1✔
1292

1293
    @property
1✔
1294
    def safe_close_rate(self) -> float:
1✔
1295
        return self.close_rate or self.close_rate_requested or 0.0
1✔
1296

1297
    @staticmethod
1✔
1298
    def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None,
1✔
1299
                         open_date: Optional[datetime] = None,
1300
                         close_date: Optional[datetime] = None,
1301
                         ) -> List['LocalTrade']:
1302
        """
1303
        Helper function to query Trades.
1304
        Returns a List of trades, filtered on the parameters given.
1305
        In live mode, converts the filter to a database query and returns all rows
1306
        In Backtest mode, uses filters on Trade.trades to get the result.
1307

1308
        :param pair: Filter by pair
1309
        :param is_open: Filter by open/closed status
1310
        :param open_date: Filter by open_date (filters via trade.open_date > input)
1311
        :param close_date: Filter by close_date (filters via trade.close_date > input)
1312
                           Will implicitly only return closed trades.
1313
        :return: unsorted List[Trade]
1314
        """
1315

1316
        # Offline mode - without database
1317
        if is_open is not None:
1✔
1318
            if is_open:
1✔
1319
                sel_trades = LocalTrade.trades_open
1✔
1320
            else:
1321
                sel_trades = LocalTrade.trades
1✔
1322

1323
        else:
1324
            # Not used during backtesting, but might be used by a strategy
1325
            sel_trades = list(LocalTrade.trades + LocalTrade.trades_open)
1✔
1326

1327
        if pair:
1✔
1328
            sel_trades = [trade for trade in sel_trades if trade.pair == pair]
1✔
1329
        if open_date:
1✔
1330
            sel_trades = [trade for trade in sel_trades if trade.open_date > open_date]
1✔
1331
        if close_date:
1✔
1332
            sel_trades = [trade for trade in sel_trades if trade.close_date
1✔
1333
                          and trade.close_date > close_date]
1334

1335
        return sel_trades
1✔
1336

1337
    @staticmethod
1✔
1338
    def close_bt_trade(trade):
1✔
1339
        LocalTrade.trades_open.remove(trade)
1✔
1340
        LocalTrade.bt_trades_open_pp[trade.pair].remove(trade)
1✔
1341
        LocalTrade.bt_open_open_trade_count -= 1
1✔
1342
        LocalTrade.trades.append(trade)
1✔
1343
        LocalTrade.total_profit += trade.close_profit_abs
1✔
1344

1345
    @staticmethod
1✔
1346
    def add_bt_trade(trade):
1✔
1347
        if trade.is_open:
1✔
1348
            LocalTrade.trades_open.append(trade)
1✔
1349
            LocalTrade.bt_trades_open_pp[trade.pair].append(trade)
1✔
1350
            LocalTrade.bt_open_open_trade_count += 1
1✔
1351
        else:
1352
            LocalTrade.trades.append(trade)
1✔
1353

1354
    @staticmethod
1✔
1355
    def remove_bt_trade(trade):
1✔
1356
        LocalTrade.trades_open.remove(trade)
1✔
1357
        LocalTrade.bt_trades_open_pp[trade.pair].remove(trade)
1✔
1358
        LocalTrade.bt_open_open_trade_count -= 1
1✔
1359

1360
    @staticmethod
1✔
1361
    def get_open_trades() -> List[Any]:
1✔
1362
        """
1363
        Retrieve open trades
1364
        """
1365
        return Trade.get_trades_proxy(is_open=True)
1✔
1366

1367
    @staticmethod
1✔
1368
    def get_open_trade_count() -> int:
1✔
1369
        """
1370
        get open trade count
1371
        """
1372
        if Trade.use_db:
1✔
1373
            return Trade.session.execute(
1✔
1374
                select(func.count(Trade.id)).filter(Trade.is_open.is_(True))
1375
            ).scalar_one()
1376
        else:
1377
            return LocalTrade.bt_open_open_trade_count
1✔
1378

1379
    @staticmethod
1✔
1380
    def stoploss_reinitialization(desired_stoploss: float):
1✔
1381
        """
1382
        Adjust initial Stoploss to desired stoploss for all open trades.
1383
        """
1384
        trade: Trade
1385
        for trade in Trade.get_open_trades():
1✔
1386
            logger.info(f"Found open trade: {trade}")
1✔
1387

1388
            # skip case if trailing-stop changed the stoploss already.
1389
            if (not trade.is_stop_loss_trailing
1✔
1390
                    and trade.initial_stop_loss_pct != desired_stoploss):
1391
                # Stoploss value got changed
1392

1393
                logger.info(f"Stoploss for {trade} needs adjustment...")
1✔
1394
                # Force reset of stoploss
1395
                trade.stop_loss = 0.0
1✔
1396
                trade.initial_stop_loss_pct = None
1✔
1397
                trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
1✔
1398
                logger.info(f"New stoploss: {trade.stop_loss}.")
1✔
1399

1400
    @classmethod
1✔
1401
    def from_json(cls, json_str: str) -> Self:
1✔
1402
        """
1403
        Create a Trade instance from a json string.
1404

1405
        Used for debugging purposes - please keep.
1406
        :param json_str: json string to parse
1407
        :return: Trade instance
1408
        """
1409
        import rapidjson
1✔
1410
        data = rapidjson.loads(json_str)
1✔
1411
        trade = cls(
1✔
1412
            __FROM_JSON=True,
1413
            id=data["trade_id"],
1414
            pair=data["pair"],
1415
            base_currency=data["base_currency"],
1416
            stake_currency=data["quote_currency"],
1417
            is_open=data["is_open"],
1418
            exchange=data["exchange"],
1419
            amount=data["amount"],
1420
            amount_requested=data["amount_requested"],
1421
            stake_amount=data["stake_amount"],
1422
            strategy=data["strategy"],
1423
            enter_tag=data["enter_tag"],
1424
            timeframe=data["timeframe"],
1425
            fee_open=data["fee_open"],
1426
            fee_open_cost=data["fee_open_cost"],
1427
            fee_open_currency=data["fee_open_currency"],
1428
            fee_close=data["fee_close"],
1429
            fee_close_cost=data["fee_close_cost"],
1430
            fee_close_currency=data["fee_close_currency"],
1431
            open_date=datetime.fromtimestamp(data["open_timestamp"] // 1000, tz=timezone.utc),
1432
            open_rate=data["open_rate"],
1433
            open_rate_requested=data["open_rate_requested"],
1434
            open_trade_value=data["open_trade_value"],
1435
            close_date=(datetime.fromtimestamp(data["close_timestamp"] // 1000, tz=timezone.utc)
1436
                        if data["close_timestamp"] else None),
1437
            realized_profit=data["realized_profit"],
1438
            close_rate=data["close_rate"],
1439
            close_rate_requested=data["close_rate_requested"],
1440
            close_profit=data["close_profit"],
1441
            close_profit_abs=data["close_profit_abs"],
1442
            exit_reason=data["exit_reason"],
1443
            exit_order_status=data["exit_order_status"],
1444
            stop_loss=data["stop_loss_abs"],
1445
            stop_loss_pct=data["stop_loss_ratio"],
1446
            initial_stop_loss=data["initial_stop_loss_abs"],
1447
            initial_stop_loss_pct=data["initial_stop_loss_ratio"],
1448
            min_rate=data["min_rate"],
1449
            max_rate=data["max_rate"],
1450
            leverage=data["leverage"],
1451
            interest_rate=data["interest_rate"],
1452
            liquidation_price=data["liquidation_price"],
1453
            is_short=data["is_short"],
1454
            trading_mode=data["trading_mode"],
1455
            funding_fees=data["funding_fees"],
1456
            amount_precision=data.get('amount_precision', None),
1457
            price_precision=data.get('price_precision', None),
1458
            precision_mode=data.get('precision_mode', None),
1459
            contract_size=data.get('contract_size', None),
1460
        )
1461
        for order in data["orders"]:
1✔
1462

1463
            order_obj = Order(
1✔
1464
                amount=order["amount"],
1465
                ft_amount=order["amount"],
1466
                ft_order_side=order["ft_order_side"],
1467
                ft_pair=order["pair"],
1468
                ft_is_open=order["is_open"],
1469
                order_id=order["order_id"],
1470
                status=order["status"],
1471
                average=order["average"],
1472
                cost=order["cost"],
1473
                filled=order["filled"],
1474
                order_date=datetime.strptime(order["order_date"], DATETIME_PRINT_FORMAT),
1475
                order_filled_date=(datetime.fromtimestamp(
1476
                    order["order_filled_timestamp"] // 1000, tz=timezone.utc)
1477
                    if order["order_filled_timestamp"] else None),
1478
                order_type=order["order_type"],
1479
                price=order["price"],
1480
                ft_price=order["price"],
1481
                remaining=order["remaining"],
1482
                funding_fee=order.get("funding_fee", None),
1483
                ft_order_tag=order.get("ft_order_tag", None),
1484
            )
1485
            trade.orders.append(order_obj)
1✔
1486

1487
        return trade
1✔
1488

1489

1490
class Trade(ModelBase, LocalTrade):
1✔
1491
    """
1492
    Trade database model.
1493
    Also handles updating and querying trades
1494

1495
    Note: Fields must be aligned with LocalTrade class
1496
    """
1497
    __tablename__ = 'trades'
1✔
1498
    session: ClassVar[SessionType]
1✔
1499

1500
    use_db: bool = True
1✔
1501

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

1504
    orders: Mapped[List[Order]] = relationship(
1✔
1505
        "Order", order_by="Order.id", cascade="all, delete-orphan", lazy="selectin",
1506
        innerjoin=True)  # type: ignore
1507
    custom_data: Mapped[List[_CustomData]] = relationship(
1✔
1508
        "_CustomData", cascade="all, delete-orphan",
1509
        lazy="raise")
1510

1511
    exchange: Mapped[str] = mapped_column(String(25), nullable=False)  # type: ignore
1✔
1512
    pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True)  # type: ignore
1✔
1513
    base_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True)  # type: ignore
1✔
1514
    stake_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True)  # type: ignore
1✔
1515
    is_open: Mapped[bool] = mapped_column(nullable=False, default=True, index=True)  # type: ignore
1✔
1516
    fee_open: Mapped[float] = mapped_column(Float(), nullable=False, default=0.0)  # type: ignore
1✔
1517
    fee_open_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)  # type: ignore
1✔
1518
    fee_open_currency: Mapped[Optional[str]] = mapped_column(
1✔
1519
        String(25), nullable=True)  # type: ignore
1520
    fee_close: Mapped[Optional[float]] = mapped_column(
1✔
1521
        Float(), nullable=False, default=0.0)  # type: ignore
1522
    fee_close_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)  # type: ignore
1✔
1523
    fee_close_currency: Mapped[Optional[str]] = mapped_column(
1✔
1524
        String(25), nullable=True)  # type: ignore
1525
    open_rate: Mapped[float] = mapped_column(Float())  # type: ignore
1✔
1526
    open_rate_requested: Mapped[Optional[float]] = mapped_column(
1✔
1527
        Float(), nullable=True)  # type: ignore
1528
    # open_trade_value - calculated via _calc_open_trade_value
1529
    open_trade_value: Mapped[float] = mapped_column(Float(), nullable=True)  # type: ignore
1✔
1530
    close_rate: Mapped[Optional[float]] = mapped_column(Float())  # type: ignore
1✔
1531
    close_rate_requested: Mapped[Optional[float]] = mapped_column(Float())  # type: ignore
1✔
1532
    realized_profit: Mapped[float] = mapped_column(
1✔
1533
        Float(), default=0.0, nullable=True)  # type: ignore
1534
    close_profit: Mapped[Optional[float]] = mapped_column(Float())  # type: ignore
1✔
1535
    close_profit_abs: Mapped[Optional[float]] = mapped_column(Float())  # type: ignore
1✔
1536
    stake_amount: Mapped[float] = mapped_column(Float(), nullable=False)  # type: ignore
1✔
1537
    max_stake_amount: Mapped[Optional[float]] = mapped_column(Float())  # type: ignore
1✔
1538
    amount: Mapped[float] = mapped_column(Float())  # type: ignore
1✔
1539
    amount_requested: Mapped[Optional[float]] = mapped_column(Float())  # type: ignore
1✔
1540
    open_date: Mapped[datetime] = mapped_column(
1✔
1541
        nullable=False, default=datetime.now)  # type: ignore
1542
    close_date: Mapped[Optional[datetime]] = mapped_column()  # type: ignore
1✔
1543
    # absolute value of the stop loss
1544
    stop_loss: Mapped[float] = mapped_column(Float(), nullable=True, default=0.0)  # type: ignore
1✔
1545
    # percentage value of the stop loss
1546
    stop_loss_pct: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)  # type: ignore
1✔
1547
    # absolute value of the initial stop loss
1548
    initial_stop_loss: Mapped[Optional[float]] = mapped_column(
1✔
1549
        Float(), nullable=True, default=0.0)  # type: ignore
1550
    # percentage value of the initial stop loss
1551
    initial_stop_loss_pct: Mapped[Optional[float]] = mapped_column(
1✔
1552
        Float(), nullable=True)  # type: ignore
1553
    is_stop_loss_trailing: Mapped[bool] = mapped_column(
1✔
1554
        nullable=False, default=False)  # type: ignore
1555
    # absolute value of the highest reached price
1556
    max_rate: Mapped[Optional[float]] = mapped_column(
1✔
1557
        Float(), nullable=True, default=0.0)  # type: ignore
1558
    # Lowest price reached
1559
    min_rate: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)  # type: ignore
1✔
1560
    exit_reason: Mapped[Optional[str]] = mapped_column(
1✔
1561
        String(CUSTOM_TAG_MAX_LENGTH), nullable=True)  # type: ignore
1562
    exit_order_status: Mapped[Optional[str]] = mapped_column(
1✔
1563
        String(100), nullable=True)  # type: ignore
1564
    strategy: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)  # type: ignore
1✔
1565
    enter_tag: Mapped[Optional[str]] = mapped_column(
1✔
1566
        String(CUSTOM_TAG_MAX_LENGTH), nullable=True)  # type: ignore
1567
    timeframe: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)  # type: ignore
1✔
1568

1569
    trading_mode: Mapped[TradingMode] = mapped_column(
1✔
1570
        Enum(TradingMode), nullable=True)  # type: ignore
1571
    amount_precision: Mapped[Optional[float]] = mapped_column(
1✔
1572
        Float(), nullable=True)  # type: ignore
1573
    price_precision: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)  # type: ignore
1✔
1574
    precision_mode: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)  # type: ignore
1✔
1575
    contract_size: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)  # type: ignore
1✔
1576

1577
    # Leverage trading properties
1578
    leverage: Mapped[float] = mapped_column(Float(), nullable=True, default=1.0)  # type: ignore
1✔
1579
    is_short: Mapped[bool] = mapped_column(nullable=False, default=False)  # type: ignore
1✔
1580
    liquidation_price: Mapped[Optional[float]] = mapped_column(
1✔
1581
        Float(), nullable=True)  # type: ignore
1582

1583
    # Margin Trading Properties
1584
    interest_rate: Mapped[float] = mapped_column(
1✔
1585
        Float(), nullable=False, default=0.0)  # type: ignore
1586

1587
    # Futures properties
1588
    funding_fees: Mapped[Optional[float]] = mapped_column(
1✔
1589
        Float(), nullable=True, default=None)  # type: ignore
1590
    funding_fee_running: Mapped[Optional[float]] = mapped_column(
1✔
1591
        Float(), nullable=True, default=None)  # type: ignore
1592

1593
    def __init__(self, **kwargs):
1✔
1594
        from_json = kwargs.pop('__FROM_JSON', None)
1✔
1595
        super().__init__(**kwargs)
1✔
1596
        if not from_json:
1✔
1597
            # Skip recalculation when loading from json
1598
            self.realized_profit = 0
1✔
1599
            self.recalc_open_trade_value()
1✔
1600

1601
    @validates('enter_tag', 'exit_reason')
1✔
1602
    def validate_string_len(self, key, value):
1✔
1603
        max_len = getattr(self.__class__, key).prop.columns[0].type.length
1✔
1604
        if value and len(value) > max_len:
1✔
1605
            return value[:max_len]
1✔
1606
        return value
1✔
1607

1608
    def delete(self) -> None:
1✔
1609

1610
        for order in self.orders:
1✔
1611
            Order.session.delete(order)
1✔
1612

1613
        CustomDataWrapper.delete_custom_data(trade_id=self.id)
1✔
1614

1615
        Trade.session.delete(self)
1✔
1616
        Trade.commit()
1✔
1617

1618
    @staticmethod
1✔
1619
    def commit():
1✔
1620
        Trade.session.commit()
1✔
1621

1622
    @staticmethod
1✔
1623
    def rollback():
1✔
1624
        Trade.session.rollback()
1✔
1625

1626
    @staticmethod
1✔
1627
    def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None,
1✔
1628
                         open_date: Optional[datetime] = None,
1629
                         close_date: Optional[datetime] = None,
1630
                         ) -> List['LocalTrade']:
1631
        """
1632
        Helper function to query Trades.j
1633
        Returns a List of trades, filtered on the parameters given.
1634
        In live mode, converts the filter to a database query and returns all rows
1635
        In Backtest mode, uses filters on Trade.trades to get the result.
1636

1637
        :return: unsorted List[Trade]
1638
        """
1639
        if Trade.use_db:
1✔
1640
            trade_filter = []
1✔
1641
            if pair:
1✔
1642
                trade_filter.append(Trade.pair == pair)
1✔
1643
            if open_date:
1✔
1644
                trade_filter.append(Trade.open_date > open_date)
1✔
1645
            if close_date:
1✔
1646
                trade_filter.append(Trade.close_date > close_date)
1✔
1647
            if is_open is not None:
1✔
1648
                trade_filter.append(Trade.is_open.is_(is_open))
1✔
1649
            return cast(List[LocalTrade], Trade.get_trades(trade_filter).all())
1✔
1650
        else:
1651
            return LocalTrade.get_trades_proxy(
1✔
1652
                pair=pair, is_open=is_open,
1653
                open_date=open_date,
1654
                close_date=close_date
1655
            )
1656

1657
    @staticmethod
1✔
1658
    def get_trades_query(trade_filter=None, include_orders: bool = True) -> Select:
1✔
1659
        """
1660
        Helper function to query Trades using filters.
1661
        NOTE: Not supported in Backtesting.
1662
        :param trade_filter: Optional filter to apply to trades
1663
                             Can be either a Filter object, or a List of filters
1664
                             e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])`
1665
                             e.g. `(trade_filter=Trade.id == trade_id)`
1666
        :return: unsorted query object
1667
        """
1668
        if not Trade.use_db:
1✔
1669
            raise NotImplementedError('`Trade.get_trades()` not supported in backtesting mode.')
1✔
1670
        if trade_filter is not None:
1✔
1671
            if not isinstance(trade_filter, list):
1✔
1672
                trade_filter = [trade_filter]
1✔
1673
            this_query = select(Trade).filter(*trade_filter)
1✔
1674
        else:
1675
            this_query = select(Trade)
1✔
1676
        if not include_orders:
1✔
1677
            # Don't load order relations
1678
            # Consider using noload or raiseload instead of lazyload
1679
            this_query = this_query.options(lazyload(Trade.orders))
1✔
1680
        return this_query
1✔
1681

1682
    @staticmethod
1✔
1683
    def get_trades(trade_filter=None, include_orders: bool = True) -> ScalarResult['Trade']:
1✔
1684
        """
1685
        Helper function to query Trades using filters.
1686
        NOTE: Not supported in Backtesting.
1687
        :param trade_filter: Optional filter to apply to trades
1688
                             Can be either a Filter object, or a List of filters
1689
                             e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])`
1690
                             e.g. `(trade_filter=Trade.id == trade_id)`
1691
        :return: unsorted query object
1692
        """
1693
        query = Trade.get_trades_query(trade_filter, include_orders)
1✔
1694
        # this should remain split. if use_db is False, session is not available and the above will
1695
        # raise an exception.
1696
        return Trade.session.scalars(query)
1✔
1697

1698
    @staticmethod
1✔
1699
    def get_open_trades_without_assigned_fees():
1✔
1700
        """
1701
        Returns all open trades which don't have open fees set correctly
1702
        NOTE: Not supported in Backtesting.
1703
        """
1704
        return Trade.get_trades([Trade.fee_open_currency.is_(None),
1✔
1705
                                 Trade.orders.any(),
1706
                                 Trade.is_open.is_(True),
1707
                                 ]).all()
1708

1709
    @staticmethod
1✔
1710
    def get_closed_trades_without_assigned_fees():
1✔
1711
        """
1712
        Returns all closed trades which don't have fees set correctly
1713
        NOTE: Not supported in Backtesting.
1714
        """
1715
        return Trade.get_trades([Trade.fee_close_currency.is_(None),
1✔
1716
                                 Trade.orders.any(),
1717
                                 Trade.is_open.is_(False),
1718
                                 ]).all()
1719

1720
    @staticmethod
1✔
1721
    def get_total_closed_profit() -> float:
1✔
1722
        """
1723
        Retrieves total realized profit
1724
        """
1725
        if Trade.use_db:
1✔
1726
            total_profit = Trade.session.execute(
1✔
1727
                select(func.sum(Trade.close_profit_abs)).filter(Trade.is_open.is_(False))
1728
            ).scalar_one()
1729
        else:
1730
            total_profit = sum(t.close_profit_abs  # type: ignore
1✔
1731
                               for t in LocalTrade.get_trades_proxy(is_open=False))
1732
        return total_profit or 0
1✔
1733

1734
    @staticmethod
1✔
1735
    def total_open_trades_stakes() -> float:
1✔
1736
        """
1737
        Calculates total invested amount in open trades
1738
        in stake currency
1739
        """
1740
        if Trade.use_db:
1✔
1741
            total_open_stake_amount = Trade.session.scalar(
1✔
1742
                select(func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True))
1743
            )
1744
        else:
1745
            total_open_stake_amount = sum(
1✔
1746
                t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True))
1747
        return total_open_stake_amount or 0
1✔
1748

1749
    @staticmethod
1✔
1750
    def get_overall_performance(minutes=None) -> List[Dict[str, Any]]:
1✔
1751
        """
1752
        Returns List of dicts containing all Trades, including profit and trade count
1753
        NOTE: Not supported in Backtesting.
1754
        """
1755
        filters: List = [Trade.is_open.is_(False)]
1✔
1756
        if minutes:
1✔
1757
            start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes)
1✔
1758
            filters.append(Trade.close_date >= start_date)
1✔
1759

1760
        pair_rates = Trade.session.execute(
1✔
1761
            select(
1762
                Trade.pair,
1763
                func.sum(Trade.close_profit).label('profit_sum'),
1764
                func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
1765
                func.count(Trade.pair).label('count')
1766
            ).filter(*filters)
1767
            .group_by(Trade.pair)
1768
            .order_by(desc('profit_sum_abs'))
1769
            ).all()
1770

1771
        return [
1✔
1772
            {
1773
                'pair': pair,
1774
                'profit_ratio': profit,
1775
                'profit': round(profit * 100, 2),  # Compatibility mode
1776
                'profit_pct': round(profit * 100, 2),
1777
                'profit_abs': profit_abs,
1778
                'count': count
1779
            }
1780
            for pair, profit, profit_abs, count in pair_rates
1781
        ]
1782

1783
    @staticmethod
1✔
1784
    def get_enter_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
1785
        """
1786
        Returns List of dicts containing all Trades, based on buy tag performance
1787
        Can either be average for all pairs or a specific pair provided
1788
        NOTE: Not supported in Backtesting.
1789
        """
1790

1791
        filters: List = [Trade.is_open.is_(False)]
1✔
1792
        if (pair is not None):
1✔
1793
            filters.append(Trade.pair == pair)
1✔
1794

1795
        enter_tag_perf = Trade.session.execute(
1✔
1796
            select(
1797
                Trade.enter_tag,
1798
                func.sum(Trade.close_profit).label('profit_sum'),
1799
                func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
1800
                func.count(Trade.pair).label('count')
1801
            ).filter(*filters)
1802
            .group_by(Trade.enter_tag)
1803
            .order_by(desc('profit_sum_abs'))
1804
        ).all()
1805

1806
        return [
1✔
1807
            {
1808
                'enter_tag': enter_tag if enter_tag is not None else "Other",
1809
                'profit_ratio': profit,
1810
                'profit_pct': round(profit * 100, 2),
1811
                'profit_abs': profit_abs,
1812
                'count': count
1813
            }
1814
            for enter_tag, profit, profit_abs, count in enter_tag_perf
1815
        ]
1816

1817
    @staticmethod
1✔
1818
    def get_exit_reason_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
1819
        """
1820
        Returns List of dicts containing all Trades, based on exit reason performance
1821
        Can either be average for all pairs or a specific pair provided
1822
        NOTE: Not supported in Backtesting.
1823
        """
1824

1825
        filters: List = [Trade.is_open.is_(False)]
1✔
1826
        if (pair is not None):
1✔
1827
            filters.append(Trade.pair == pair)
1✔
1828
        sell_tag_perf = Trade.session.execute(
1✔
1829
            select(
1830
                Trade.exit_reason,
1831
                func.sum(Trade.close_profit).label('profit_sum'),
1832
                func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
1833
                func.count(Trade.pair).label('count')
1834
            ).filter(*filters)
1835
            .group_by(Trade.exit_reason)
1836
            .order_by(desc('profit_sum_abs'))
1837
        ).all()
1838

1839
        return [
1✔
1840
            {
1841
                'exit_reason': exit_reason if exit_reason is not None else "Other",
1842
                'profit_ratio': profit,
1843
                'profit_pct': round(profit * 100, 2),
1844
                'profit_abs': profit_abs,
1845
                'count': count
1846
            }
1847
            for exit_reason, profit, profit_abs, count in sell_tag_perf
1848
        ]
1849

1850
    @staticmethod
1✔
1851
    def get_mix_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
1852
        """
1853
        Returns List of dicts containing all Trades, based on entry_tag + exit_reason performance
1854
        Can either be average for all pairs or a specific pair provided
1855
        NOTE: Not supported in Backtesting.
1856
        """
1857

1858
        filters: List = [Trade.is_open.is_(False)]
1✔
1859
        if (pair is not None):
1✔
1860
            filters.append(Trade.pair == pair)
1✔
1861
        mix_tag_perf = Trade.session.execute(
1✔
1862
            select(
1863
                Trade.id,
1864
                Trade.enter_tag,
1865
                Trade.exit_reason,
1866
                func.sum(Trade.close_profit).label('profit_sum'),
1867
                func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
1868
                func.count(Trade.pair).label('count')
1869
            ).filter(*filters)
1870
            .group_by(Trade.id)
1871
            .order_by(desc('profit_sum_abs'))
1872
        ).all()
1873

1874
        resp: List[Dict] = []
1✔
1875
        for id, enter_tag, exit_reason, profit, profit_abs, count in mix_tag_perf:
1✔
1876
            enter_tag = enter_tag if enter_tag is not None else "Other"
1✔
1877
            exit_reason = exit_reason if exit_reason is not None else "Other"
1✔
1878

1879
            if (exit_reason is not None and enter_tag is not None):
1✔
1880
                mix_tag = enter_tag + " " + exit_reason
1✔
1881
                i = 0
1✔
1882
                if not any(item["mix_tag"] == mix_tag for item in resp):
1✔
1883
                    resp.append({'mix_tag': mix_tag,
1✔
1884
                                 'profit_ratio': profit,
1885
                                 'profit_pct': round(profit * 100, 2),
1886
                                 'profit_abs': profit_abs,
1887
                                 'count': count})
1888
                else:
1889
                    while i < len(resp):
×
1890
                        if resp[i]["mix_tag"] == mix_tag:
×
1891
                            resp[i] = {
×
1892
                                'mix_tag': mix_tag,
1893
                                'profit_ratio': profit + resp[i]["profit_ratio"],
1894
                                'profit_pct': round(profit + resp[i]["profit_ratio"] * 100, 2),
1895
                                'profit_abs': profit_abs + resp[i]["profit_abs"],
1896
                                'count': 1 + resp[i]["count"]
1897
                            }
1898
                        i += 1
×
1899

1900
        return resp
1✔
1901

1902
    @staticmethod
1✔
1903
    def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)):
1✔
1904
        """
1905
        Get best pair with closed trade.
1906
        NOTE: Not supported in Backtesting.
1907
        :returns: Tuple containing (pair, profit_sum)
1908
        """
1909
        best_pair = Trade.session.execute(
1✔
1910
            select(
1911
                Trade.pair,
1912
                func.sum(Trade.close_profit).label('profit_sum')
1913
            ).filter(Trade.is_open.is_(False) & (Trade.close_date >= start_date))
1914
            .group_by(Trade.pair)
1915
            .order_by(desc('profit_sum'))
1916
        ).first()
1917

1918
        return best_pair
1✔
1919

1920
    @staticmethod
1✔
1921
    def get_trading_volume(start_date: datetime = datetime.fromtimestamp(0)) -> float:
1✔
1922
        """
1923
        Get Trade volume based on Orders
1924
        NOTE: Not supported in Backtesting.
1925
        :returns: Tuple containing (pair, profit_sum)
1926
        """
1927
        trading_volume = Trade.session.execute(
1✔
1928
            select(
1929
                func.sum(Order.cost).label('volume')
1930
            ).filter(
1931
                Order.order_filled_date >= start_date,
1932
                Order.status == 'closed'
1933
            )).scalar_one()
1934
        return trading_volume or 0.0
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