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

freqtrade / freqtrade / 4131167254

pending completion
4131167254

push

github-actions

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

16866 of 17748 relevant lines covered (95.03%)

0.95 hits per line

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

98.4
/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 datetime import datetime, timedelta, timezone
1✔
7
from math import isclose
1✔
8
from typing import Any, Dict, List, Optional
1✔
9

10
from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
1✔
11
                        UniqueConstraint, desc, func)
12
from sqlalchemy.orm import Query, lazyload, relationship
1✔
13

14
from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES,
1✔
15
                                 BuySell, LongShort)
16
from freqtrade.enums import ExitType, TradingMode
1✔
17
from freqtrade.exceptions import DependencyException, OperationalException
1✔
18
from freqtrade.exchange import amount_to_contract_precision, price_to_precision
1✔
19
from freqtrade.leverage import interest
1✔
20
from freqtrade.persistence.base import _DECL_BASE
1✔
21
from freqtrade.util import FtPrecise
1✔
22

23

24
logger = logging.getLogger(__name__)
1✔
25

26

27
class Order(_DECL_BASE):
1✔
28
    """
29
    Order database model
30
    Keeps a record of all orders placed on the exchange
31

32
    One to many relationship with Trades:
33
      - One trade can have many orders
34
      - One Order can only be associated with one Trade
35

36
    Mirrors CCXT Order structure
37
    """
38
    __tablename__ = 'orders'
1✔
39
    # Uniqueness should be ensured over pair, order_id
40
    # its likely that order_id is unique per Pair on some exchanges.
41
    __table_args__ = (UniqueConstraint('ft_pair', 'order_id', name="_order_pair_order_id"),)
1✔
42

43
    id = Column(Integer, primary_key=True)
1✔
44
    ft_trade_id = Column(Integer, ForeignKey('trades.id'), index=True)
1✔
45

46
    trade = relationship("Trade", back_populates="orders")
1✔
47

48
    # order_side can only be 'buy', 'sell' or 'stoploss'
49
    ft_order_side: str = Column(String(25), nullable=False)
1✔
50
    ft_pair: str = Column(String(25), nullable=False)
1✔
51
    ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
1✔
52

53
    order_id: str = Column(String(255), nullable=False, index=True)
1✔
54
    status = Column(String(255), nullable=True)
1✔
55
    symbol = Column(String(25), nullable=True)
1✔
56
    order_type: str = Column(String(50), nullable=True)
1✔
57
    side = Column(String(25), nullable=True)
1✔
58
    price = Column(Float, nullable=True)
1✔
59
    average = Column(Float, nullable=True)
1✔
60
    amount = Column(Float, nullable=True)
1✔
61
    filled = Column(Float, nullable=True)
1✔
62
    remaining = Column(Float, nullable=True)
1✔
63
    cost = Column(Float, nullable=True)
1✔
64
    stop_price = Column(Float, nullable=True)
1✔
65
    order_date = Column(DateTime, nullable=True, default=datetime.utcnow)
1✔
66
    order_filled_date = Column(DateTime, nullable=True)
1✔
67
    order_update_date = Column(DateTime, nullable=True)
1✔
68

69
    funding_fee = Column(Float, nullable=True)
1✔
70

71
    ft_fee_base = Column(Float, nullable=True)
1✔
72

73
    @property
1✔
74
    def order_date_utc(self) -> datetime:
1✔
75
        """ Order-date with UTC timezoneinfo"""
76
        return self.order_date.replace(tzinfo=timezone.utc)
1✔
77

78
    @property
1✔
79
    def order_filled_utc(self) -> Optional[datetime]:
1✔
80
        """ last order-date with UTC timezoneinfo"""
81
        return (
1✔
82
            self.order_filled_date.replace(tzinfo=timezone.utc) if self.order_filled_date else None
83
        )
84

85
    @property
1✔
86
    def safe_price(self) -> float:
1✔
87
        return self.average or self.price or self.stop_price
1✔
88

89
    @property
1✔
90
    def safe_filled(self) -> float:
1✔
91
        return self.filled if self.filled is not None else self.amount or 0.0
1✔
92

93
    @property
1✔
94
    def safe_remaining(self) -> float:
1✔
95
        return (
1✔
96
            self.remaining if self.remaining is not None else
97
            self.amount - (self.filled or 0.0)
98
        )
99

100
    @property
1✔
101
    def safe_fee_base(self) -> float:
1✔
102
        return self.ft_fee_base or 0.0
1✔
103

104
    @property
1✔
105
    def safe_amount_after_fee(self) -> float:
1✔
106
        return self.safe_filled - self.safe_fee_base
1✔
107

108
    def __repr__(self):
1✔
109

110
        return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, '
1✔
111
                f'side={self.side}, order_type={self.order_type}, status={self.status})')
112

113
    def update_from_ccxt_object(self, order):
1✔
114
        """
115
        Update Order from ccxt response
116
        Only updates if fields are available from ccxt -
117
        """
118
        if self.order_id != str(order['id']):
1✔
119
            raise DependencyException("Order-id's don't match")
1✔
120

121
        self.status = order.get('status', self.status)
1✔
122
        self.symbol = order.get('symbol', self.symbol)
1✔
123
        self.order_type = order.get('type', self.order_type)
1✔
124
        self.side = order.get('side', self.side)
1✔
125
        self.price = order.get('price', self.price)
1✔
126
        self.amount = order.get('amount', self.amount)
1✔
127
        self.filled = order.get('filled', self.filled)
1✔
128
        self.average = order.get('average', self.average)
1✔
129
        self.remaining = order.get('remaining', self.remaining)
1✔
130
        self.cost = order.get('cost', self.cost)
1✔
131
        self.stop_price = order.get('stopPrice', self.stop_price)
1✔
132

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

136
        self.ft_is_open = True
1✔
137
        if self.status in NON_OPEN_EXCHANGE_STATES:
1✔
138
            self.ft_is_open = False
1✔
139
            if self.trade:
1✔
140
                # Assign funding fee up to this point
141
                # (represents the funding fee since the last order)
142
                self.funding_fee = self.trade.funding_fees
1✔
143
            if (order.get('filled', 0.0) or 0.0) > 0:
1✔
144
                self.order_filled_date = datetime.now(timezone.utc)
1✔
145
        self.order_update_date = datetime.now(timezone.utc)
1✔
146

147
    def to_ccxt_object(self) -> Dict[str, Any]:
1✔
148
        return {
1✔
149
            'id': self.order_id,
150
            'symbol': self.ft_pair,
151
            'price': self.price,
152
            'average': self.average,
153
            'amount': self.amount,
154
            'cost': self.cost,
155
            'type': self.order_type,
156
            'side': self.ft_order_side,
157
            'filled': self.filled,
158
            'remaining': self.remaining,
159
            'stopPrice': self.stop_price,
160
            'datetime': self.order_date_utc.strftime('%Y-%m-%dT%H:%M:%S.%f'),
161
            'timestamp': int(self.order_date_utc.timestamp() * 1000),
162
            'status': self.status,
163
            'fee': None,
164
            'info': {},
165
        }
166

167
    def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]:
1✔
168
        resp = {
1✔
169
            'amount': self.amount,
170
            'safe_price': self.safe_price,
171
            'ft_order_side': self.ft_order_side,
172
            'order_filled_timestamp': int(self.order_filled_date.replace(
173
                tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
174
            'ft_is_entry': self.ft_order_side == entry_side,
175
        }
176
        if not minified:
1✔
177
            resp.update({
1✔
178
                'pair': self.ft_pair,
179
                'order_id': self.order_id,
180
                'status': self.status,
181
                'average': round(self.average, 8) if self.average else 0,
182
                'cost': self.cost if self.cost else 0,
183
                'filled': self.filled,
184
                'is_open': self.ft_is_open,
185
                'order_date': self.order_date.strftime(DATETIME_PRINT_FORMAT)
186
                if self.order_date else None,
187
                'order_timestamp': int(self.order_date.replace(
188
                    tzinfo=timezone.utc).timestamp() * 1000) if self.order_date else None,
189
                'order_filled_date': self.order_filled_date.strftime(DATETIME_PRINT_FORMAT)
190
                if self.order_filled_date else None,
191
                'order_type': self.order_type,
192
                'price': self.price,
193
                'remaining': self.remaining,
194
            })
195
        return resp
1✔
196

197
    def close_bt_order(self, close_date: datetime, trade: 'LocalTrade'):
1✔
198
        self.order_filled_date = close_date
1✔
199
        self.filled = self.amount
1✔
200
        self.remaining = 0
1✔
201
        self.status = 'closed'
1✔
202
        self.ft_is_open = False
1✔
203
        # Assign funding fees to Order.
204
        # Assumes backtesting will use date_last_filled_utc to calculate future funding fees.
205
        self.funding_fee = trade.funding_fees
1✔
206

207
        if (self.ft_order_side == trade.entry_side):
1✔
208
            trade.open_rate = self.price
1✔
209
            trade.recalc_trade_from_orders()
1✔
210
            trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True)
1✔
211

212
    @staticmethod
1✔
213
    def update_orders(orders: List['Order'], order: Dict[str, Any]):
1✔
214
        """
215
        Get all non-closed orders - useful when trying to batch-update orders
216
        """
217
        if not isinstance(order, dict):
1✔
218
            logger.warning(f"{order} is not a valid response object.")
1✔
219
            return
1✔
220

221
        filtered_orders = [o for o in orders if o.order_id == order.get('id')]
1✔
222
        if filtered_orders:
1✔
223
            oobj = filtered_orders[0]
1✔
224
            oobj.update_from_ccxt_object(order)
1✔
225
            Trade.commit()
1✔
226
        else:
227
            logger.warning(f"Did not find order for {order}.")
1✔
228

229
    @staticmethod
1✔
230
    def parse_from_ccxt_object(order: Dict[str, Any], pair: str, side: str) -> 'Order':
1✔
231
        """
232
        Parse an order from a ccxt object and return a new order Object.
233
        """
234
        o = Order(order_id=str(order['id']), ft_order_side=side, ft_pair=pair)
1✔
235

236
        o.update_from_ccxt_object(order)
1✔
237
        return o
1✔
238

239
    @staticmethod
1✔
240
    def get_open_orders() -> List['Order']:
1✔
241
        """
242
        Retrieve open orders from the database
243
        :return: List of open orders
244
        """
245
        return Order.query.filter(Order.ft_is_open.is_(True)).all()
1✔
246

247
    @staticmethod
1✔
248
    def order_by_id(order_id: str) -> Optional['Order']:
1✔
249
        """
250
        Retrieve order based on order_id
251
        :return: Order or None
252
        """
253
        return Order.query.filter(Order.order_id == order_id).first()
1✔
254

255

256
class LocalTrade():
1✔
257
    """
258
    Trade database model.
259
    Used in backtesting - must be aligned to Trade model!
260

261
    """
262
    use_db: bool = False
1✔
263
    # Trades container for backtesting
264
    trades: List['LocalTrade'] = []
1✔
265
    trades_open: List['LocalTrade'] = []
1✔
266
    # Copy of trades_open - but indexed by pair
267
    bt_trades_open_pp: Dict[str, List['LocalTrade']] = defaultdict(list)
1✔
268
    bt_open_open_trade_count: int = 0
1✔
269
    total_profit: float = 0
1✔
270
    realized_profit: float = 0
1✔
271

272
    id: int = 0
1✔
273

274
    orders: List[Order] = []
1✔
275

276
    exchange: str = ''
1✔
277
    pair: str = ''
1✔
278
    base_currency: str = ''
1✔
279
    stake_currency: str = ''
1✔
280
    is_open: bool = True
1✔
281
    fee_open: float = 0.0
1✔
282
    fee_open_cost: Optional[float] = None
1✔
283
    fee_open_currency: str = ''
1✔
284
    fee_close: float = 0.0
1✔
285
    fee_close_cost: Optional[float] = None
1✔
286
    fee_close_currency: str = ''
1✔
287
    open_rate: float = 0.0
1✔
288
    open_rate_requested: Optional[float] = None
1✔
289
    # open_trade_value - calculated via _calc_open_trade_value
290
    open_trade_value: float = 0.0
1✔
291
    close_rate: Optional[float] = None
1✔
292
    close_rate_requested: Optional[float] = None
1✔
293
    close_profit: Optional[float] = None
1✔
294
    close_profit_abs: Optional[float] = None
1✔
295
    stake_amount: float = 0.0
1✔
296
    max_stake_amount: float = 0.0
1✔
297
    amount: float = 0.0
1✔
298
    amount_requested: Optional[float] = None
1✔
299
    open_date: datetime
1✔
300
    close_date: Optional[datetime] = None
1✔
301
    open_order_id: Optional[str] = None
1✔
302
    # absolute value of the stop loss
303
    stop_loss: float = 0.0
1✔
304
    # percentage value of the stop loss
305
    stop_loss_pct: float = 0.0
1✔
306
    # absolute value of the initial stop loss
307
    initial_stop_loss: float = 0.0
1✔
308
    # percentage value of the initial stop loss
309
    initial_stop_loss_pct: Optional[float] = None
1✔
310
    # stoploss order id which is on exchange
311
    stoploss_order_id: Optional[str] = None
1✔
312
    # last update time of the stoploss order on exchange
313
    stoploss_last_update: Optional[datetime] = None
1✔
314
    # absolute value of the highest reached price
315
    max_rate: float = 0.0
1✔
316
    # Lowest price reached
317
    min_rate: float = 0.0
1✔
318
    exit_reason: str = ''
1✔
319
    exit_order_status: str = ''
1✔
320
    strategy: str = ''
1✔
321
    enter_tag: Optional[str] = None
1✔
322
    timeframe: Optional[int] = None
1✔
323

324
    trading_mode: TradingMode = TradingMode.SPOT
1✔
325
    amount_precision: Optional[float] = None
1✔
326
    price_precision: Optional[float] = None
1✔
327
    precision_mode: Optional[int] = None
1✔
328
    contract_size: Optional[float] = None
1✔
329

330
    # Leverage trading properties
331
    liquidation_price: Optional[float] = None
1✔
332
    is_short: bool = False
1✔
333
    leverage: float = 1.0
1✔
334

335
    # Margin trading properties
336
    interest_rate: float = 0.0
1✔
337

338
    # Futures properties
339
    funding_fees: Optional[float] = None
1✔
340

341
    @property
1✔
342
    def stoploss_or_liquidation(self) -> float:
1✔
343
        if self.liquidation_price:
1✔
344
            if self.is_short:
1✔
345
                return min(self.stop_loss, self.liquidation_price)
1✔
346
            else:
347
                return max(self.stop_loss, self.liquidation_price)
1✔
348

349
        return self.stop_loss
1✔
350

351
    @property
1✔
352
    def buy_tag(self) -> Optional[str]:
1✔
353
        """
354
        Compatibility between buy_tag (old) and enter_tag (new)
355
        Consider buy_tag deprecated
356
        """
357
        return self.enter_tag
1✔
358

359
    @property
1✔
360
    def has_no_leverage(self) -> bool:
1✔
361
        """Returns true if this is a non-leverage, non-short trade"""
362
        return ((self.leverage == 1.0 or self.leverage is None) and not self.is_short)
1✔
363

364
    @property
1✔
365
    def borrowed(self) -> float:
1✔
366
        """
367
            The amount of currency borrowed from the exchange for leverage trades
368
            If a long trade, the amount is in base currency
369
            If a short trade, the amount is in the other currency being traded
370
        """
371
        if self.has_no_leverage:
1✔
372
            return 0.0
1✔
373
        elif not self.is_short:
1✔
374
            return (self.amount * self.open_rate) * ((self.leverage - 1) / self.leverage)
1✔
375
        else:
376
            return self.amount
1✔
377

378
    @property
1✔
379
    def date_last_filled_utc(self) -> datetime:
1✔
380
        """ Date of the last filled order"""
381
        orders = self.select_filled_orders()
1✔
382
        if not orders:
1✔
383
            return self.open_date_utc
1✔
384
        return max([self.open_date_utc,
1✔
385
                    max(o.order_filled_utc for o in orders if o.order_filled_utc)])
386

387
    @property
1✔
388
    def open_date_utc(self):
1✔
389
        return self.open_date.replace(tzinfo=timezone.utc)
1✔
390

391
    @property
1✔
392
    def stoploss_last_update_utc(self):
1✔
393
        if self.stoploss_last_update:
1✔
394
            return self.stoploss_last_update.replace(tzinfo=timezone.utc)
1✔
395
        return None
×
396

397
    @property
1✔
398
    def close_date_utc(self):
1✔
399
        return self.close_date.replace(tzinfo=timezone.utc)
1✔
400

401
    @property
1✔
402
    def entry_side(self) -> str:
1✔
403
        if self.is_short:
1✔
404
            return "sell"
1✔
405
        else:
406
            return "buy"
1✔
407

408
    @property
1✔
409
    def exit_side(self) -> BuySell:
1✔
410
        if self.is_short:
1✔
411
            return "buy"
1✔
412
        else:
413
            return "sell"
1✔
414

415
    @property
1✔
416
    def trade_direction(self) -> LongShort:
1✔
417
        if self.is_short:
1✔
418
            return "short"
1✔
419
        else:
420
            return "long"
1✔
421

422
    @property
1✔
423
    def safe_base_currency(self) -> str:
1✔
424
        """
425
        Compatibility layer for asset - which can be empty for old trades.
426
        """
427
        try:
1✔
428
            return self.base_currency or self.pair.split('/')[0]
1✔
429
        except IndexError:
×
430
            return ''
×
431

432
    @property
1✔
433
    def safe_quote_currency(self) -> str:
1✔
434
        """
435
        Compatibility layer for asset - which can be empty for old trades.
436
        """
437
        try:
1✔
438
            return self.stake_currency or self.pair.split('/')[1].split(':')[0]
1✔
439
        except IndexError:
×
440
            return ''
×
441

442
    def __init__(self, **kwargs):
1✔
443
        for key in kwargs:
1✔
444
            setattr(self, key, kwargs[key])
1✔
445
        self.recalc_open_trade_value()
1✔
446
        if self.trading_mode == TradingMode.MARGIN and self.interest_rate is None:
1✔
447
            raise OperationalException(
×
448
                f"{self.trading_mode.value} trading requires param interest_rate on trades")
449

450
    def __repr__(self):
1✔
451
        open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
1✔
452

453
        return (
1✔
454
            f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
455
            f'is_short={self.is_short or False}, leverage={self.leverage or 1.0}, '
456
            f'open_rate={self.open_rate:.8f}, open_since={open_since})'
457
        )
458

459
    def to_json(self, minified: bool = False) -> Dict[str, Any]:
1✔
460
        filled_orders = self.select_filled_or_open_orders()
1✔
461
        orders = [order.to_json(self.entry_side, minified) for order in filled_orders]
1✔
462

463
        return {
1✔
464
            'trade_id': self.id,
465
            'pair': self.pair,
466
            'base_currency': self.safe_base_currency,
467
            'quote_currency': self.safe_quote_currency,
468
            'is_open': self.is_open,
469
            'exchange': self.exchange,
470
            'amount': round(self.amount, 8),
471
            'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
472
            'stake_amount': round(self.stake_amount, 8),
473
            'max_stake_amount': round(self.max_stake_amount, 8) if self.max_stake_amount else None,
474
            'strategy': self.strategy,
475
            'enter_tag': self.enter_tag,
476
            'timeframe': self.timeframe,
477

478
            'fee_open': self.fee_open,
479
            'fee_open_cost': self.fee_open_cost,
480
            'fee_open_currency': self.fee_open_currency,
481
            'fee_close': self.fee_close,
482
            'fee_close_cost': self.fee_close_cost,
483
            'fee_close_currency': self.fee_close_currency,
484

485
            'open_date': self.open_date.strftime(DATETIME_PRINT_FORMAT),
486
            'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000),
487
            'open_rate': self.open_rate,
488
            'open_rate_requested': self.open_rate_requested,
489
            'open_trade_value': round(self.open_trade_value, 8),
490

491
            'close_date': (self.close_date.strftime(DATETIME_PRINT_FORMAT)
492
                           if self.close_date else None),
493
            'close_timestamp': int(self.close_date.replace(
494
                tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None,
495
            'realized_profit': self.realized_profit or 0.0,
496
            'close_rate': self.close_rate,
497
            'close_rate_requested': self.close_rate_requested,
498
            'close_profit': self.close_profit,  # Deprecated
499
            'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
500
            'close_profit_abs': self.close_profit_abs,  # Deprecated
501

502
            'trade_duration_s': (int((self.close_date_utc - self.open_date_utc).total_seconds())
503
                                 if self.close_date else None),
504
            'trade_duration': (int((self.close_date_utc - self.open_date_utc).total_seconds() // 60)
505
                               if self.close_date else None),
506

507
            'profit_ratio': self.close_profit,
508
            'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
509
            'profit_abs': self.close_profit_abs,
510

511
            'exit_reason': self.exit_reason,
512
            'exit_order_status': self.exit_order_status,
513
            'stop_loss_abs': self.stop_loss,
514
            'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
515
            'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
516
            'stoploss_order_id': self.stoploss_order_id,
517
            'stoploss_last_update': (self.stoploss_last_update.strftime(DATETIME_PRINT_FORMAT)
518
                                     if self.stoploss_last_update else None),
519
            'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace(
520
                tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None,
521
            'initial_stop_loss_abs': self.initial_stop_loss,
522
            'initial_stop_loss_ratio': (self.initial_stop_loss_pct
523
                                        if self.initial_stop_loss_pct else None),
524
            'initial_stop_loss_pct': (self.initial_stop_loss_pct * 100
525
                                      if self.initial_stop_loss_pct else None),
526
            'min_rate': self.min_rate,
527
            'max_rate': self.max_rate,
528

529
            'leverage': self.leverage,
530
            'interest_rate': self.interest_rate,
531
            'liquidation_price': self.liquidation_price,
532
            'is_short': self.is_short,
533
            'trading_mode': self.trading_mode,
534
            'funding_fees': self.funding_fees,
535
            'open_order_id': self.open_order_id,
536
            'orders': orders,
537
        }
538

539
    @staticmethod
1✔
540
    def reset_trades() -> None:
1✔
541
        """
542
        Resets all trades. Only active for backtesting mode.
543
        """
544
        LocalTrade.trades = []
1✔
545
        LocalTrade.trades_open = []
1✔
546
        LocalTrade.bt_trades_open_pp = defaultdict(list)
1✔
547
        LocalTrade.bt_open_open_trade_count = 0
1✔
548
        LocalTrade.total_profit = 0
1✔
549

550
    def adjust_min_max_rates(self, current_price: float, current_price_low: float) -> None:
1✔
551
        """
552
        Adjust the max_rate and min_rate.
553
        """
554
        self.max_rate = max(current_price, self.max_rate or self.open_rate)
1✔
555
        self.min_rate = min(current_price_low, self.min_rate or self.open_rate)
1✔
556

557
    def set_liquidation_price(self, liquidation_price: Optional[float]):
1✔
558
        """
559
        Method you should use to set self.liquidation price.
560
        Assures stop_loss is not passed the liquidation price
561
        """
562
        if not liquidation_price:
1✔
563
            return
1✔
564
        self.liquidation_price = liquidation_price
1✔
565

566
    def __set_stop_loss(self, stop_loss: float, percent: float):
1✔
567
        """
568
        Method used internally to set self.stop_loss.
569
        """
570
        stop_loss_norm = price_to_precision(stop_loss, self.price_precision, self.precision_mode)
1✔
571
        if not self.stop_loss:
1✔
572
            self.initial_stop_loss = stop_loss_norm
1✔
573
        self.stop_loss = stop_loss_norm
1✔
574

575
        self.stop_loss_pct = -1 * abs(percent)
1✔
576

577
    def adjust_stop_loss(self, current_price: float, stoploss: float,
1✔
578
                         initial: bool = False, refresh: bool = False) -> None:
579
        """
580
        This adjusts the stop loss to it's most recently observed setting
581
        :param current_price: Current rate the asset is traded
582
        :param stoploss: Stoploss as factor (sample -0.05 -> -5% below current price).
583
        :param initial: Called to initiate stop_loss.
584
            Skips everything if self.stop_loss is already set.
585
        """
586
        if initial and not (self.stop_loss is None or self.stop_loss == 0):
1✔
587
            # Don't modify if called with initial and nothing to do
588
            return
1✔
589
        refresh = True if refresh and self.nr_of_successful_entries == 1 else False
1✔
590

591
        leverage = self.leverage or 1.0
1✔
592
        if self.is_short:
1✔
593
            new_loss = float(current_price * (1 + abs(stoploss / leverage)))
1✔
594
        else:
595
            new_loss = float(current_price * (1 - abs(stoploss / leverage)))
1✔
596

597
        # no stop loss assigned yet
598
        if self.initial_stop_loss_pct is None or refresh:
1✔
599
            self.__set_stop_loss(new_loss, stoploss)
1✔
600
            self.initial_stop_loss = price_to_precision(
1✔
601
                new_loss, self.price_precision, self.precision_mode)
602
            self.initial_stop_loss_pct = -1 * abs(stoploss)
1✔
603

604
        # evaluate if the stop loss needs to be updated
605
        else:
606

607
            higher_stop = new_loss > self.stop_loss
1✔
608
            lower_stop = new_loss < self.stop_loss
1✔
609

610
            # stop losses only walk up, never down!,
611
            #   ? But adding more to a leveraged trade would create a lower liquidation price,
612
            #   ? decreasing the minimum stoploss
613
            if (higher_stop and not self.is_short) or (lower_stop and self.is_short):
1✔
614
                logger.debug(f"{self.pair} - Adjusting stoploss...")
1✔
615
                self.__set_stop_loss(new_loss, stoploss)
1✔
616
            else:
617
                logger.debug(f"{self.pair} - Keeping current stoploss...")
1✔
618

619
        logger.debug(
1✔
620
            f"{self.pair} - Stoploss adjusted. current_price={current_price:.8f}, "
621
            f"open_rate={self.open_rate:.8f}, max_rate={self.max_rate or self.open_rate:.8f}, "
622
            f"initial_stop_loss={self.initial_stop_loss:.8f}, "
623
            f"stop_loss={self.stop_loss:.8f}. "
624
            f"Trailing stoploss saved us: "
625
            f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.")
626

627
    def update_trade(self, order: Order) -> None:
1✔
628
        """
629
        Updates this entity with amount and actual open/close rates.
630
        :param order: order retrieved by exchange.fetch_order()
631
        :return: None
632
        """
633

634
        # Ignore open and cancelled orders
635
        if order.status == 'open' or order.safe_price is None:
1✔
636
            return
1✔
637

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

640
        if order.ft_order_side == self.entry_side:
1✔
641
            # Update open rate and actual amount
642
            self.open_rate = order.safe_price
1✔
643
            self.amount = order.safe_amount_after_fee
1✔
644
            if self.is_open:
1✔
645
                payment = "SELL" if self.is_short else "BUY"
1✔
646
                logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.')
1✔
647
            # condition to avoid reset value when updating fees
648
            if self.open_order_id == order.order_id:
1✔
649
                self.open_order_id = None
1✔
650
            else:
651
                logger.warning(
1✔
652
                    f'Got different open_order_id {self.open_order_id} != {order.order_id}')
653
            self.recalc_trade_from_orders()
1✔
654
        elif order.ft_order_side == self.exit_side:
1✔
655
            if self.is_open:
1✔
656
                payment = "BUY" if self.is_short else "SELL"
1✔
657
                # * On margin shorts, you buy a little bit more than the amount (amount + interest)
658
                logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.')
1✔
659
            # condition to avoid reset value when updating fees
660
            if self.open_order_id == order.order_id:
1✔
661
                self.open_order_id = None
1✔
662
            else:
663
                logger.warning(
1✔
664
                    f'Got different open_order_id {self.open_order_id} != {order.order_id}')
665
            amount_tr = amount_to_contract_precision(self.amount, self.amount_precision,
1✔
666
                                                     self.precision_mode, self.contract_size)
667
            if isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC):
1✔
668
                self.close(order.safe_price)
1✔
669
            else:
670
                self.recalc_trade_from_orders()
1✔
671
        elif order.ft_order_side == 'stoploss' and order.status not in ('canceled', 'open'):
1✔
672
            self.stoploss_order_id = None
1✔
673
            self.close_rate_requested = self.stop_loss
1✔
674
            self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
1✔
675
            if self.is_open:
1✔
676
                logger.info(f'{order.order_type.upper()} is hit for {self}.')
1✔
677
            self.close(order.safe_price)
1✔
678
        else:
679
            raise ValueError(f'Unknown order type: {order.order_type}')
1✔
680
        Trade.commit()
1✔
681

682
    def close(self, rate: float, *, show_msg: bool = True) -> None:
1✔
683
        """
684
        Sets close_rate to the given rate, calculates total profit
685
        and marks trade as closed
686
        """
687
        self.close_rate = rate
1✔
688
        self.close_date = self.close_date or datetime.utcnow()
1✔
689
        self.is_open = False
1✔
690
        self.exit_order_status = 'closed'
1✔
691
        self.open_order_id = None
1✔
692
        self.recalc_trade_from_orders(is_closing=True)
1✔
693
        if show_msg:
1✔
694
            logger.info(
1✔
695
                'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
696
                self
697
            )
698

699
    def update_fee(self, fee_cost: float, fee_currency: Optional[str], fee_rate: Optional[float],
1✔
700
                   side: str) -> None:
701
        """
702
        Update Fee parameters. Only acts once per side
703
        """
704
        if self.entry_side == side and self.fee_open_currency is None:
1✔
705
            self.fee_open_cost = fee_cost
1✔
706
            self.fee_open_currency = fee_currency
1✔
707
            if fee_rate is not None:
1✔
708
                self.fee_open = fee_rate
1✔
709
                # Assume close-fee will fall into the same fee category and take an educated guess
710
                self.fee_close = fee_rate
1✔
711
        elif self.exit_side == side and self.fee_close_currency is None:
1✔
712
            self.fee_close_cost = fee_cost
1✔
713
            self.fee_close_currency = fee_currency
1✔
714
            if fee_rate is not None:
1✔
715
                self.fee_close = fee_rate
1✔
716

717
    def fee_updated(self, side: str) -> bool:
1✔
718
        """
719
        Verify if this side (buy / sell) has already been updated
720
        """
721
        if self.entry_side == side:
1✔
722
            return self.fee_open_currency is not None
1✔
723
        elif self.exit_side == side:
1✔
724
            return self.fee_close_currency is not None
1✔
725
        else:
726
            return False
1✔
727

728
    def update_order(self, order: Dict) -> None:
1✔
729
        Order.update_orders(self.orders, order)
1✔
730

731
    def get_exit_order_count(self) -> int:
1✔
732
        """
733
        Get amount of failed exiting orders
734
        assumes full exits.
735
        """
736
        return len([o for o in self.orders if o.ft_order_side == self.exit_side])
1✔
737

738
    def _calc_open_trade_value(self, amount: float, open_rate: float) -> float:
1✔
739
        """
740
        Calculate the open_rate including open_fee.
741
        :return: Price in of the open trade incl. Fees
742
        """
743
        open_trade = FtPrecise(amount) * FtPrecise(open_rate)
1✔
744
        fees = open_trade * FtPrecise(self.fee_open)
1✔
745
        if self.is_short:
1✔
746
            return float(open_trade - fees)
1✔
747
        else:
748
            return float(open_trade + fees)
1✔
749

750
    def recalc_open_trade_value(self) -> None:
1✔
751
        """
752
        Recalculate open_trade_value.
753
        Must be called whenever open_rate, fee_open is changed.
754
        """
755
        self.open_trade_value = self._calc_open_trade_value(self.amount, self.open_rate)
1✔
756

757
    def calculate_interest(self) -> FtPrecise:
1✔
758
        """
759
        Calculate interest for this trade. Only applicable for Margin trading.
760
        """
761
        zero = FtPrecise(0.0)
1✔
762
        # If nothing was borrowed
763
        if self.trading_mode != TradingMode.MARGIN or self.has_no_leverage:
1✔
764
            return zero
1✔
765

766
        open_date = self.open_date.replace(tzinfo=None)
1✔
767
        now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None)
1✔
768
        sec_per_hour = FtPrecise(3600)
1✔
769
        total_seconds = FtPrecise((now - open_date).total_seconds())
1✔
770
        hours = total_seconds / sec_per_hour or zero
1✔
771

772
        rate = FtPrecise(self.interest_rate)
1✔
773
        borrowed = FtPrecise(self.borrowed)
1✔
774

775
        return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
1✔
776

777
    def _calc_base_close(self, amount: FtPrecise, rate: float, fee: float) -> FtPrecise:
1✔
778

779
        close_trade = amount * FtPrecise(rate)
1✔
780
        fees = close_trade * FtPrecise(fee)
1✔
781

782
        if self.is_short:
1✔
783
            return close_trade + fees
1✔
784
        else:
785
            return close_trade - fees
1✔
786

787
    def calc_close_trade_value(self, rate: float, amount: float = None) -> float:
1✔
788
        """
789
        Calculate the Trade's close value including fees
790
        :param rate: rate to compare with.
791
        :return: value in stake currency of the open trade
792
        """
793
        if rate is None and not self.close_rate:
1✔
794
            return 0.0
1✔
795

796
        amount1 = FtPrecise(amount or self.amount)
1✔
797
        trading_mode = self.trading_mode or TradingMode.SPOT
1✔
798

799
        if trading_mode == TradingMode.SPOT:
1✔
800
            return float(self._calc_base_close(amount1, rate, self.fee_close))
1✔
801

802
        elif (trading_mode == TradingMode.MARGIN):
1✔
803

804
            total_interest = self.calculate_interest()
1✔
805

806
            if self.is_short:
1✔
807
                amount1 = amount1 + total_interest
1✔
808
                return float(self._calc_base_close(amount1, rate, self.fee_close))
1✔
809
            else:
810
                # Currency already owned for longs, no need to purchase
811
                return float(self._calc_base_close(amount1, rate, self.fee_close) - total_interest)
1✔
812

813
        elif (trading_mode == TradingMode.FUTURES):
1✔
814
            funding_fees = self.funding_fees or 0.0
1✔
815
            # Positive funding_fees -> Trade has gained from fees.
816
            # Negative funding_fees -> Trade had to pay the fees.
817
            if self.is_short:
1✔
818
                return float(self._calc_base_close(amount1, rate, self.fee_close)) - funding_fees
1✔
819
            else:
820
                return float(self._calc_base_close(amount1, rate, self.fee_close)) + funding_fees
1✔
821
        else:
822
            raise OperationalException(
×
823
                f"{self.trading_mode.value} trading is not yet available using freqtrade")
824

825
    def calc_profit(self, rate: float, amount: float = None, open_rate: float = None) -> float:
1✔
826
        """
827
        Calculate the absolute profit in stake currency between Close and Open trade
828
        :param rate: close rate to compare with.
829
        :param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
830
        :param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
831
        :return: profit in stake currency as float
832
        """
833
        close_trade_value = self.calc_close_trade_value(rate, amount)
1✔
834
        if amount is None or open_rate is None:
1✔
835
            open_trade_value = self.open_trade_value
1✔
836
        else:
837
            open_trade_value = self._calc_open_trade_value(amount, open_rate)
1✔
838

839
        if self.is_short:
1✔
840
            profit = open_trade_value - close_trade_value
1✔
841
        else:
842
            profit = close_trade_value - open_trade_value
1✔
843
        return float(f"{profit:.8f}")
1✔
844

845
    def calc_profit_ratio(
1✔
846
            self, rate: float, amount: float = None, open_rate: float = None) -> float:
847
        """
848
        Calculates the profit as ratio (including fee).
849
        :param rate: rate to compare with.
850
        :param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
851
        :param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
852
        :return: profit ratio as float
853
        """
854
        close_trade_value = self.calc_close_trade_value(rate, amount)
1✔
855

856
        if amount is None or open_rate is None:
1✔
857
            open_trade_value = self.open_trade_value
1✔
858
        else:
859
            open_trade_value = self._calc_open_trade_value(amount, open_rate)
1✔
860

861
        short_close_zero = (self.is_short and close_trade_value == 0.0)
1✔
862
        long_close_zero = (not self.is_short and open_trade_value == 0.0)
1✔
863
        leverage = self.leverage or 1.0
1✔
864

865
        if (short_close_zero or long_close_zero):
1✔
866
            return 0.0
1✔
867
        else:
868
            if self.is_short:
1✔
869
                profit_ratio = (1 - (close_trade_value / open_trade_value)) * leverage
1✔
870
            else:
871
                profit_ratio = ((close_trade_value / open_trade_value) - 1) * leverage
1✔
872

873
        return float(f"{profit_ratio:.8f}")
1✔
874

875
    def recalc_trade_from_orders(self, *, is_closing: bool = False):
1✔
876
        ZERO = FtPrecise(0.0)
1✔
877
        current_amount = FtPrecise(0.0)
1✔
878
        current_stake = FtPrecise(0.0)
1✔
879
        max_stake_amount = FtPrecise(0.0)
1✔
880
        total_stake = 0.0  # Total stake after all buy orders (does not subtract!)
1✔
881
        avg_price = FtPrecise(0.0)
1✔
882
        close_profit = 0.0
1✔
883
        close_profit_abs = 0.0
1✔
884
        profit = None
1✔
885
        # Reset funding fees
886
        self.funding_fees = 0.0
1✔
887
        funding_fees = 0.0
1✔
888
        ordercount = len(self.orders) - 1
1✔
889
        for i, o in enumerate(self.orders):
1✔
890
            if o.ft_is_open or not o.filled:
1✔
891
                continue
1✔
892
            funding_fees += (o.funding_fee or 0.0)
1✔
893
            tmp_amount = FtPrecise(o.safe_amount_after_fee)
1✔
894
            tmp_price = FtPrecise(o.safe_price)
1✔
895

896
            is_exit = o.ft_order_side != self.entry_side
1✔
897
            side = FtPrecise(-1 if is_exit else 1)
1✔
898
            if tmp_amount > ZERO and tmp_price is not None:
1✔
899
                current_amount += tmp_amount * side
1✔
900
                price = avg_price if is_exit else tmp_price
1✔
901
                current_stake += price * tmp_amount * side
1✔
902

903
                if current_amount > ZERO:
1✔
904
                    avg_price = current_stake / current_amount
1✔
905

906
            if is_exit:
1✔
907
                # Process exits
908
                if i == ordercount and is_closing:
1✔
909
                    # Apply funding fees only to the last closing order
910
                    self.funding_fees = funding_fees
1✔
911

912
                exit_rate = o.safe_price
1✔
913
                exit_amount = o.safe_amount_after_fee
1✔
914
                profit = self.calc_profit(rate=exit_rate, amount=exit_amount,
1✔
915
                                          open_rate=float(avg_price))
916
                close_profit_abs += profit
1✔
917
                close_profit = self.calc_profit_ratio(
1✔
918
                    exit_rate, amount=exit_amount, open_rate=avg_price)
919
            else:
920
                total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price)
1✔
921
                max_stake_amount += (tmp_amount * price)
1✔
922
        self.funding_fees = funding_fees
1✔
923
        self.max_stake_amount = float(max_stake_amount)
1✔
924

925
        if close_profit:
1✔
926
            self.close_profit = close_profit
1✔
927
            self.realized_profit = close_profit_abs
1✔
928
            self.close_profit_abs = profit
1✔
929

930
        current_amount_tr = amount_to_contract_precision(
1✔
931
            float(current_amount), self.amount_precision, self.precision_mode, self.contract_size)
932
        if current_amount_tr > 0.0:
1✔
933
            # Trade is still open
934
            # Leverage not updated, as we don't allow changing leverage through DCA at the moment.
935
            self.open_rate = float(current_stake / current_amount)
1✔
936
            self.amount = current_amount_tr
1✔
937
            self.stake_amount = float(current_stake) / (self.leverage or 1.0)
1✔
938
            self.fee_open_cost = self.fee_open * float(current_stake)
1✔
939
            self.recalc_open_trade_value()
1✔
940
            if self.stop_loss_pct is not None and self.open_rate is not None:
1✔
941
                self.adjust_stop_loss(self.open_rate, self.stop_loss_pct)
1✔
942
        elif is_closing and total_stake > 0:
1✔
943
            # Close profit abs / maximum owned
944
            # Fees are considered as they are part of close_profit_abs
945
            self.close_profit = (close_profit_abs / total_stake) * self.leverage
1✔
946
            self.close_profit_abs = close_profit_abs
1✔
947

948
    def select_order_by_order_id(self, order_id: str) -> Optional[Order]:
1✔
949
        """
950
        Finds order object by Order id.
951
        :param order_id: Exchange order id
952
        """
953
        for o in self.orders:
1✔
954
            if o.order_id == order_id:
1✔
955
                return o
1✔
956
        return None
1✔
957

958
    def select_order(self, order_side: Optional[str] = None,
1✔
959
                     is_open: Optional[bool] = None) -> Optional[Order]:
960
        """
961
        Finds latest order for this orderside and status
962
        :param order_side: ft_order_side of the order (either 'buy', 'sell' or 'stoploss')
963
        :param is_open: Only search for open orders?
964
        :return: latest Order object if it exists, else None
965
        """
966
        orders = self.orders
1✔
967
        if order_side:
1✔
968
            orders = [o for o in orders if o.ft_order_side == order_side]
1✔
969
        if is_open is not None:
1✔
970
            orders = [o for o in orders if o.ft_is_open == is_open]
1✔
971
        if len(orders) > 0:
1✔
972
            return orders[-1]
1✔
973
        else:
974
            return None
1✔
975

976
    def select_filled_orders(self, order_side: Optional[str] = None) -> List['Order']:
1✔
977
        """
978
        Finds filled orders for this orderside.
979
        :param order_side: Side of the order (either 'buy', 'sell', or None)
980
        :return: array of Order objects
981
        """
982
        return [o for o in self.orders if ((o.ft_order_side == order_side) or (order_side is None))
1✔
983
                and o.ft_is_open is False
984
                and o.filled
985
                and o.status in NON_OPEN_EXCHANGE_STATES]
986

987
    def select_filled_or_open_orders(self) -> List['Order']:
1✔
988
        """
989
        Finds filled or open orders
990
        :param order_side: Side of the order (either 'buy', 'sell', or None)
991
        :return: array of Order objects
992
        """
993
        return [o for o in self.orders if
1✔
994
                (
995
                    o.ft_is_open is False
996
                    and (o.filled or 0) > 0
997
                    and o.status in NON_OPEN_EXCHANGE_STATES
998
                    )
999
                or (o.ft_is_open is True and o.status is not None)
1000
                ]
1001

1002
    @property
1✔
1003
    def nr_of_successful_entries(self) -> int:
1✔
1004
        """
1005
        Helper function to count the number of entry orders that have been filled.
1006
        :return: int count of entry orders that have been filled for this trade.
1007
        """
1008

1009
        return len(self.select_filled_orders(self.entry_side))
1✔
1010

1011
    @property
1✔
1012
    def nr_of_successful_exits(self) -> int:
1✔
1013
        """
1014
        Helper function to count the number of exit orders that have been filled.
1015
        :return: int count of exit orders that have been filled for this trade.
1016
        """
1017
        return len(self.select_filled_orders(self.exit_side))
1✔
1018

1019
    @property
1✔
1020
    def nr_of_successful_buys(self) -> int:
1✔
1021
        """
1022
        Helper function to count the number of buy orders that have been filled.
1023
        WARNING: Please use nr_of_successful_entries for short support.
1024
        :return: int count of buy orders that have been filled for this trade.
1025
        """
1026

1027
        return len(self.select_filled_orders('buy'))
1✔
1028

1029
    @property
1✔
1030
    def nr_of_successful_sells(self) -> int:
1✔
1031
        """
1032
        Helper function to count the number of sell orders that have been filled.
1033
        WARNING: Please use nr_of_successful_exits for short support.
1034
        :return: int count of sell orders that have been filled for this trade.
1035
        """
1036
        return len(self.select_filled_orders('sell'))
×
1037

1038
    @property
1✔
1039
    def sell_reason(self) -> str:
1✔
1040
        """ DEPRECATED! Please use exit_reason instead."""
1041
        return self.exit_reason
1✔
1042

1043
    @staticmethod
1✔
1044
    def get_trades_proxy(*, pair: str = None, is_open: bool = None,
1✔
1045
                         open_date: datetime = None, close_date: datetime = None,
1046
                         ) -> List['LocalTrade']:
1047
        """
1048
        Helper function to query Trades.
1049
        Returns a List of trades, filtered on the parameters given.
1050
        In live mode, converts the filter to a database query and returns all rows
1051
        In Backtest mode, uses filters on Trade.trades to get the result.
1052

1053
        :return: unsorted List[Trade]
1054
        """
1055

1056
        # Offline mode - without database
1057
        if is_open is not None:
1✔
1058
            if is_open:
1✔
1059
                sel_trades = LocalTrade.trades_open
1✔
1060
            else:
1061
                sel_trades = LocalTrade.trades
1✔
1062

1063
        else:
1064
            # Not used during backtesting, but might be used by a strategy
1065
            sel_trades = list(LocalTrade.trades + LocalTrade.trades_open)
1✔
1066

1067
        if pair:
1✔
1068
            sel_trades = [trade for trade in sel_trades if trade.pair == pair]
1✔
1069
        if open_date:
1✔
1070
            sel_trades = [trade for trade in sel_trades if trade.open_date > open_date]
1✔
1071
        if close_date:
1✔
1072
            sel_trades = [trade for trade in sel_trades if trade.close_date
1✔
1073
                          and trade.close_date > close_date]
1074

1075
        return sel_trades
1✔
1076

1077
    @staticmethod
1✔
1078
    def close_bt_trade(trade):
1✔
1079
        LocalTrade.trades_open.remove(trade)
1✔
1080
        LocalTrade.bt_trades_open_pp[trade.pair].remove(trade)
1✔
1081
        LocalTrade.bt_open_open_trade_count -= 1
1✔
1082
        LocalTrade.trades.append(trade)
1✔
1083
        LocalTrade.total_profit += trade.close_profit_abs
1✔
1084

1085
    @staticmethod
1✔
1086
    def add_bt_trade(trade):
1✔
1087
        if trade.is_open:
1✔
1088
            LocalTrade.trades_open.append(trade)
1✔
1089
            LocalTrade.bt_trades_open_pp[trade.pair].append(trade)
1✔
1090
            LocalTrade.bt_open_open_trade_count += 1
1✔
1091
        else:
1092
            LocalTrade.trades.append(trade)
1✔
1093

1094
    @staticmethod
1✔
1095
    def remove_bt_trade(trade):
1✔
1096
        LocalTrade.trades_open.remove(trade)
1✔
1097
        LocalTrade.bt_trades_open_pp[trade.pair].remove(trade)
1✔
1098
        LocalTrade.bt_open_open_trade_count -= 1
1✔
1099

1100
    @staticmethod
1✔
1101
    def get_open_trades() -> List[Any]:
1✔
1102
        """
1103
        Query trades from persistence layer
1104
        """
1105
        return Trade.get_trades_proxy(is_open=True)
1✔
1106

1107
    @staticmethod
1✔
1108
    def get_open_trade_count() -> int:
1✔
1109
        """
1110
        get open trade count
1111
        """
1112
        if Trade.use_db:
1✔
1113
            return Trade.query.filter(Trade.is_open.is_(True)).count()
1✔
1114
        else:
1115
            return LocalTrade.bt_open_open_trade_count
1✔
1116

1117
    @staticmethod
1✔
1118
    def stoploss_reinitialization(desired_stoploss):
1✔
1119
        """
1120
        Adjust initial Stoploss to desired stoploss for all open trades.
1121
        """
1122
        for trade in Trade.get_open_trades():
1✔
1123
            logger.info("Found open trade: %s", trade)
1✔
1124

1125
            # skip case if trailing-stop changed the stoploss already.
1126
            if (trade.stop_loss == trade.initial_stop_loss
1✔
1127
                    and trade.initial_stop_loss_pct != desired_stoploss):
1128
                # Stoploss value got changed
1129

1130
                logger.info(f"Stoploss for {trade} needs adjustment...")
1✔
1131
                # Force reset of stoploss
1132
                trade.stop_loss = None
1✔
1133
                trade.initial_stop_loss_pct = None
1✔
1134
                trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
1✔
1135
                logger.info(f"New stoploss: {trade.stop_loss}.")
1✔
1136

1137

1138
class Trade(_DECL_BASE, LocalTrade):
1✔
1139
    """
1140
    Trade database model.
1141
    Also handles updating and querying trades
1142

1143
    Note: Fields must be aligned with LocalTrade class
1144
    """
1145
    __tablename__ = 'trades'
1✔
1146

1147
    use_db: bool = True
1✔
1148

1149
    id = Column(Integer, primary_key=True)
1✔
1150

1151
    orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan",
1✔
1152
                          lazy="selectin", innerjoin=True)
1153

1154
    exchange = Column(String(25), nullable=False)
1✔
1155
    pair = Column(String(25), nullable=False, index=True)
1✔
1156
    base_currency = Column(String(25), nullable=True)
1✔
1157
    stake_currency = Column(String(25), nullable=True)
1✔
1158
    is_open = Column(Boolean, nullable=False, default=True, index=True)
1✔
1159
    fee_open = Column(Float, nullable=False, default=0.0)
1✔
1160
    fee_open_cost = Column(Float, nullable=True)
1✔
1161
    fee_open_currency = Column(String(25), nullable=True)
1✔
1162
    fee_close = Column(Float, nullable=False, default=0.0)
1✔
1163
    fee_close_cost = Column(Float, nullable=True)
1✔
1164
    fee_close_currency = Column(String(25), nullable=True)
1✔
1165
    open_rate: float = Column(Float)
1✔
1166
    open_rate_requested = Column(Float)
1✔
1167
    # open_trade_value - calculated via _calc_open_trade_value
1168
    open_trade_value = Column(Float)
1✔
1169
    close_rate: Optional[float] = Column(Float)
1✔
1170
    close_rate_requested = Column(Float)
1✔
1171
    realized_profit = Column(Float, default=0.0)
1✔
1172
    close_profit = Column(Float)
1✔
1173
    close_profit_abs = Column(Float)
1✔
1174
    stake_amount = Column(Float, nullable=False)
1✔
1175
    max_stake_amount = Column(Float)
1✔
1176
    amount = Column(Float)
1✔
1177
    amount_requested = Column(Float)
1✔
1178
    open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
1✔
1179
    close_date = Column(DateTime)
1✔
1180
    open_order_id = Column(String(255))
1✔
1181
    # absolute value of the stop loss
1182
    stop_loss = Column(Float, nullable=True, default=0.0)
1✔
1183
    # percentage value of the stop loss
1184
    stop_loss_pct = Column(Float, nullable=True)
1✔
1185
    # absolute value of the initial stop loss
1186
    initial_stop_loss = Column(Float, nullable=True, default=0.0)
1✔
1187
    # percentage value of the initial stop loss
1188
    initial_stop_loss_pct = Column(Float, nullable=True)
1✔
1189
    # stoploss order id which is on exchange
1190
    stoploss_order_id = Column(String(255), nullable=True, index=True)
1✔
1191
    # last update time of the stoploss order on exchange
1192
    stoploss_last_update = Column(DateTime, nullable=True)
1✔
1193
    # absolute value of the highest reached price
1194
    max_rate = Column(Float, nullable=True, default=0.0)
1✔
1195
    # Lowest price reached
1196
    min_rate = Column(Float, nullable=True)
1✔
1197
    exit_reason = Column(String(100), nullable=True)
1✔
1198
    exit_order_status = Column(String(100), nullable=True)
1✔
1199
    strategy = Column(String(100), nullable=True)
1✔
1200
    enter_tag = Column(String(100), nullable=True)
1✔
1201
    timeframe = Column(Integer, nullable=True)
1✔
1202

1203
    trading_mode = Column(Enum(TradingMode), nullable=True)
1✔
1204
    amount_precision = Column(Float, nullable=True)
1✔
1205
    price_precision = Column(Float, nullable=True)
1✔
1206
    precision_mode = Column(Integer, nullable=True)
1✔
1207
    contract_size = Column(Float, nullable=True)
1✔
1208

1209
    # Leverage trading properties
1210
    leverage = Column(Float, nullable=True, default=1.0)
1✔
1211
    is_short = Column(Boolean, nullable=False, default=False)
1✔
1212
    liquidation_price = Column(Float, nullable=True)
1✔
1213

1214
    # Margin Trading Properties
1215
    interest_rate = Column(Float, nullable=False, default=0.0)
1✔
1216

1217
    # Futures properties
1218
    funding_fees = Column(Float, nullable=True, default=None)
1✔
1219

1220
    def __init__(self, **kwargs):
1✔
1221
        super().__init__(**kwargs)
1✔
1222
        self.realized_profit = 0
1✔
1223
        self.recalc_open_trade_value()
1✔
1224

1225
    def delete(self) -> None:
1✔
1226

1227
        for order in self.orders:
1✔
1228
            Order.query.session.delete(order)
1✔
1229

1230
        Trade.query.session.delete(self)
1✔
1231
        Trade.commit()
1✔
1232

1233
    @staticmethod
1✔
1234
    def commit():
1✔
1235
        Trade.query.session.commit()
1✔
1236

1237
    @staticmethod
1✔
1238
    def rollback():
1✔
1239
        Trade.query.session.rollback()
1✔
1240

1241
    @staticmethod
1✔
1242
    def get_trades_proxy(*, pair: str = None, is_open: bool = None,
1✔
1243
                         open_date: datetime = None, close_date: datetime = None,
1244
                         ) -> List['LocalTrade']:
1245
        """
1246
        Helper function to query Trades.j
1247
        Returns a List of trades, filtered on the parameters given.
1248
        In live mode, converts the filter to a database query and returns all rows
1249
        In Backtest mode, uses filters on Trade.trades to get the result.
1250

1251
        :return: unsorted List[Trade]
1252
        """
1253
        if Trade.use_db:
1✔
1254
            trade_filter = []
1✔
1255
            if pair:
1✔
1256
                trade_filter.append(Trade.pair == pair)
1✔
1257
            if open_date:
1✔
1258
                trade_filter.append(Trade.open_date > open_date)
1✔
1259
            if close_date:
1✔
1260
                trade_filter.append(Trade.close_date > close_date)
1✔
1261
            if is_open is not None:
1✔
1262
                trade_filter.append(Trade.is_open.is_(is_open))
1✔
1263
            return Trade.get_trades(trade_filter).all()
1✔
1264
        else:
1265
            return LocalTrade.get_trades_proxy(
1✔
1266
                pair=pair, is_open=is_open,
1267
                open_date=open_date,
1268
                close_date=close_date
1269
            )
1270

1271
    @staticmethod
1✔
1272
    def get_trades(trade_filter=None, include_orders: bool = True) -> Query:
1✔
1273
        """
1274
        Helper function to query Trades using filters.
1275
        NOTE: Not supported in Backtesting.
1276
        :param trade_filter: Optional filter to apply to trades
1277
                             Can be either a Filter object, or a List of filters
1278
                             e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])`
1279
                             e.g. `(trade_filter=Trade.id == trade_id)`
1280
        :return: unsorted query object
1281
        """
1282
        if not Trade.use_db:
1✔
1283
            raise NotImplementedError('`Trade.get_trades()` not supported in backtesting mode.')
1✔
1284
        if trade_filter is not None:
1✔
1285
            if not isinstance(trade_filter, list):
1✔
1286
                trade_filter = [trade_filter]
1✔
1287
            this_query = Trade.query.filter(*trade_filter)
1✔
1288
        else:
1289
            this_query = Trade.query
1✔
1290
        if not include_orders:
1✔
1291
            # Don't load order relations
1292
            # Consider using noload or raiseload instead of lazyload
1293
            this_query = this_query.options(lazyload(Trade.orders))
1✔
1294
        return this_query
1✔
1295

1296
    @staticmethod
1✔
1297
    def get_open_order_trades() -> List['Trade']:
1✔
1298
        """
1299
        Returns all open trades
1300
        NOTE: Not supported in Backtesting.
1301
        """
1302
        return Trade.get_trades(Trade.open_order_id.isnot(None)).all()
1✔
1303

1304
    @staticmethod
1✔
1305
    def get_open_trades_without_assigned_fees():
1✔
1306
        """
1307
        Returns all open trades which don't have open fees set correctly
1308
        NOTE: Not supported in Backtesting.
1309
        """
1310
        return Trade.get_trades([Trade.fee_open_currency.is_(None),
1✔
1311
                                 Trade.orders.any(),
1312
                                 Trade.is_open.is_(True),
1313
                                 ]).all()
1314

1315
    @staticmethod
1✔
1316
    def get_closed_trades_without_assigned_fees():
1✔
1317
        """
1318
        Returns all closed trades which don't have fees set correctly
1319
        NOTE: Not supported in Backtesting.
1320
        """
1321
        return Trade.get_trades([Trade.fee_close_currency.is_(None),
1✔
1322
                                 Trade.orders.any(),
1323
                                 Trade.is_open.is_(False),
1324
                                 ]).all()
1325

1326
    @staticmethod
1✔
1327
    def get_total_closed_profit() -> float:
1✔
1328
        """
1329
        Retrieves total realized profit
1330
        """
1331
        if Trade.use_db:
1✔
1332
            total_profit = Trade.query.with_entities(
1✔
1333
                func.sum(Trade.close_profit_abs)).filter(Trade.is_open.is_(False)).scalar()
1334
        else:
1335
            total_profit = sum(
1✔
1336
                t.close_profit_abs for t in LocalTrade.get_trades_proxy(is_open=False))
1337
        return total_profit or 0
1✔
1338

1339
    @staticmethod
1✔
1340
    def total_open_trades_stakes() -> float:
1✔
1341
        """
1342
        Calculates total invested amount in open trades
1343
        in stake currency
1344
        """
1345
        if Trade.use_db:
1✔
1346
            total_open_stake_amount = Trade.query.with_entities(
1✔
1347
                func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar()
1348
        else:
1349
            total_open_stake_amount = sum(
1✔
1350
                t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True))
1351
        return total_open_stake_amount or 0
1✔
1352

1353
    @staticmethod
1✔
1354
    def get_overall_performance(minutes=None) -> List[Dict[str, Any]]:
1✔
1355
        """
1356
        Returns List of dicts containing all Trades, including profit and trade count
1357
        NOTE: Not supported in Backtesting.
1358
        """
1359
        filters = [Trade.is_open.is_(False)]
1✔
1360
        if minutes:
1✔
1361
            start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes)
1✔
1362
            filters.append(Trade.close_date >= start_date)
1✔
1363
        pair_rates = Trade.query.with_entities(
1✔
1364
            Trade.pair,
1365
            func.sum(Trade.close_profit).label('profit_sum'),
1366
            func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
1367
            func.count(Trade.pair).label('count')
1368
        ).filter(*filters)\
1369
            .group_by(Trade.pair) \
1370
            .order_by(desc('profit_sum_abs')) \
1371
            .all()
1372
        return [
1✔
1373
            {
1374
                'pair': pair,
1375
                'profit_ratio': profit,
1376
                'profit': round(profit * 100, 2),  # Compatibility mode
1377
                'profit_pct': round(profit * 100, 2),
1378
                'profit_abs': profit_abs,
1379
                'count': count
1380
            }
1381
            for pair, profit, profit_abs, count in pair_rates
1382
        ]
1383

1384
    @staticmethod
1✔
1385
    def get_enter_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
1386
        """
1387
        Returns List of dicts containing all Trades, based on buy tag performance
1388
        Can either be average for all pairs or a specific pair provided
1389
        NOTE: Not supported in Backtesting.
1390
        """
1391

1392
        filters = [Trade.is_open.is_(False)]
1✔
1393
        if (pair is not None):
1✔
1394
            filters.append(Trade.pair == pair)
1✔
1395

1396
        enter_tag_perf = Trade.query.with_entities(
1✔
1397
            Trade.enter_tag,
1398
            func.sum(Trade.close_profit).label('profit_sum'),
1399
            func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
1400
            func.count(Trade.pair).label('count')
1401
        ).filter(*filters)\
1402
            .group_by(Trade.enter_tag) \
1403
            .order_by(desc('profit_sum_abs')) \
1404
            .all()
1405

1406
        return [
1✔
1407
            {
1408
                'enter_tag': enter_tag if enter_tag is not None else "Other",
1409
                'profit_ratio': profit,
1410
                'profit_pct': round(profit * 100, 2),
1411
                'profit_abs': profit_abs,
1412
                'count': count
1413
            }
1414
            for enter_tag, profit, profit_abs, count in enter_tag_perf
1415
        ]
1416

1417
    @staticmethod
1✔
1418
    def get_exit_reason_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
1419
        """
1420
        Returns List of dicts containing all Trades, based on exit reason performance
1421
        Can either be average for all pairs or a specific pair provided
1422
        NOTE: Not supported in Backtesting.
1423
        """
1424

1425
        filters = [Trade.is_open.is_(False)]
1✔
1426
        if (pair is not None):
1✔
1427
            filters.append(Trade.pair == pair)
1✔
1428

1429
        sell_tag_perf = Trade.query.with_entities(
1✔
1430
            Trade.exit_reason,
1431
            func.sum(Trade.close_profit).label('profit_sum'),
1432
            func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
1433
            func.count(Trade.pair).label('count')
1434
        ).filter(*filters)\
1435
            .group_by(Trade.exit_reason) \
1436
            .order_by(desc('profit_sum_abs')) \
1437
            .all()
1438

1439
        return [
1✔
1440
            {
1441
                'exit_reason': exit_reason if exit_reason is not None else "Other",
1442
                'profit_ratio': profit,
1443
                'profit_pct': round(profit * 100, 2),
1444
                'profit_abs': profit_abs,
1445
                'count': count
1446
            }
1447
            for exit_reason, profit, profit_abs, count in sell_tag_perf
1448
        ]
1449

1450
    @staticmethod
1✔
1451
    def get_mix_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
1✔
1452
        """
1453
        Returns List of dicts containing all Trades, based on entry_tag + exit_reason performance
1454
        Can either be average for all pairs or a specific pair provided
1455
        NOTE: Not supported in Backtesting.
1456
        """
1457

1458
        filters = [Trade.is_open.is_(False)]
1✔
1459
        if (pair is not None):
1✔
1460
            filters.append(Trade.pair == pair)
1✔
1461

1462
        mix_tag_perf = Trade.query.with_entities(
1✔
1463
            Trade.id,
1464
            Trade.enter_tag,
1465
            Trade.exit_reason,
1466
            func.sum(Trade.close_profit).label('profit_sum'),
1467
            func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
1468
            func.count(Trade.pair).label('count')
1469
        ).filter(*filters)\
1470
            .group_by(Trade.id) \
1471
            .order_by(desc('profit_sum_abs')) \
1472
            .all()
1473

1474
        return_list: List[Dict] = []
1✔
1475
        for id, enter_tag, exit_reason, profit, profit_abs, count in mix_tag_perf:
1✔
1476
            enter_tag = enter_tag if enter_tag is not None else "Other"
1✔
1477
            exit_reason = exit_reason if exit_reason is not None else "Other"
1✔
1478

1479
            if (exit_reason is not None and enter_tag is not None):
1✔
1480
                mix_tag = enter_tag + " " + exit_reason
1✔
1481
                i = 0
1✔
1482
                if not any(item["mix_tag"] == mix_tag for item in return_list):
1✔
1483
                    return_list.append({'mix_tag': mix_tag,
1✔
1484
                                        'profit': profit,
1485
                                        'profit_pct': round(profit * 100, 2),
1486
                                        'profit_abs': profit_abs,
1487
                                        'count': count})
1488
                else:
1489
                    while i < len(return_list):
×
1490
                        if return_list[i]["mix_tag"] == mix_tag:
×
1491
                            return_list[i] = {
×
1492
                                'mix_tag': mix_tag,
1493
                                'profit': profit + return_list[i]["profit"],
1494
                                'profit_pct': round(profit + return_list[i]["profit"] * 100, 2),
1495
                                'profit_abs': profit_abs + return_list[i]["profit_abs"],
1496
                                'count': 1 + return_list[i]["count"]}
1497
                        i += 1
×
1498

1499
        return return_list
1✔
1500

1501
    @staticmethod
1✔
1502
    def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)):
1✔
1503
        """
1504
        Get best pair with closed trade.
1505
        NOTE: Not supported in Backtesting.
1506
        :returns: Tuple containing (pair, profit_sum)
1507
        """
1508
        best_pair = Trade.query.with_entities(
1✔
1509
            Trade.pair, func.sum(Trade.close_profit).label('profit_sum')
1510
        ).filter(Trade.is_open.is_(False) & (Trade.close_date >= start_date)) \
1511
            .group_by(Trade.pair) \
1512
            .order_by(desc('profit_sum')).first()
1513
        return best_pair
1✔
1514

1515
    @staticmethod
1✔
1516
    def get_trading_volume(start_date: datetime = datetime.fromtimestamp(0)) -> float:
1✔
1517
        """
1518
        Get Trade volume based on Orders
1519
        NOTE: Not supported in Backtesting.
1520
        :returns: Tuple containing (pair, profit_sum)
1521
        """
1522
        trading_volume = Order.query.with_entities(
1✔
1523
            func.sum(Order.cost).label('volume')
1524
        ).filter(
1525
            Order.order_filled_date >= start_date,
1526
            Order.status == 'closed'
1527
        ).scalar()
1528
        return trading_volume
1✔
1529

1530
    @staticmethod
1✔
1531
    def from_json(json_str: str) -> 'Trade':
1✔
1532
        """
1533
        Create a Trade instance from a json string.
1534

1535
        Used for debugging purposes - please keep.
1536
        :param json_str: json string to parse
1537
        :return: Trade instance
1538
        """
1539
        import rapidjson
1✔
1540
        data = rapidjson.loads(json_str)
1✔
1541
        trade = Trade(
1✔
1542
            id=data["trade_id"],
1543
            pair=data["pair"],
1544
            base_currency=data["base_currency"],
1545
            stake_currency=data["quote_currency"],
1546
            is_open=data["is_open"],
1547
            exchange=data["exchange"],
1548
            amount=data["amount"],
1549
            amount_requested=data["amount_requested"],
1550
            stake_amount=data["stake_amount"],
1551
            strategy=data["strategy"],
1552
            enter_tag=data["enter_tag"],
1553
            timeframe=data["timeframe"],
1554
            fee_open=data["fee_open"],
1555
            fee_open_cost=data["fee_open_cost"],
1556
            fee_open_currency=data["fee_open_currency"],
1557
            fee_close=data["fee_close"],
1558
            fee_close_cost=data["fee_close_cost"],
1559
            fee_close_currency=data["fee_close_currency"],
1560
            open_date=datetime.fromtimestamp(data["open_timestamp"] // 1000, tz=timezone.utc),
1561
            open_rate=data["open_rate"],
1562
            open_rate_requested=data["open_rate_requested"],
1563
            open_trade_value=data["open_trade_value"],
1564
            close_date=(datetime.fromtimestamp(data["close_timestamp"] // 1000, tz=timezone.utc)
1565
                        if data["close_timestamp"] else None),
1566
            realized_profit=data["realized_profit"],
1567
            close_rate=data["close_rate"],
1568
            close_rate_requested=data["close_rate_requested"],
1569
            close_profit=data["close_profit"],
1570
            close_profit_abs=data["close_profit_abs"],
1571
            exit_reason=data["exit_reason"],
1572
            exit_order_status=data["exit_order_status"],
1573
            stop_loss=data["stop_loss_abs"],
1574
            stop_loss_pct=data["stop_loss_ratio"],
1575
            stoploss_order_id=data["stoploss_order_id"],
1576
            stoploss_last_update=(datetime.fromtimestamp(data["stoploss_last_update"] // 1000,
1577
                                  tz=timezone.utc) if data["stoploss_last_update"] else None),
1578
            initial_stop_loss=data["initial_stop_loss_abs"],
1579
            initial_stop_loss_pct=data["initial_stop_loss_ratio"],
1580
            min_rate=data["min_rate"],
1581
            max_rate=data["max_rate"],
1582
            leverage=data["leverage"],
1583
            interest_rate=data["interest_rate"],
1584
            liquidation_price=data["liquidation_price"],
1585
            is_short=data["is_short"],
1586
            trading_mode=data["trading_mode"],
1587
            funding_fees=data["funding_fees"],
1588
            open_order_id=data["open_order_id"],
1589
        )
1590
        for order in data["orders"]:
1✔
1591

1592
            order_obj = Order(
1✔
1593
                amount=order["amount"],
1594
                ft_order_side=order["ft_order_side"],
1595
                ft_pair=order["pair"],
1596
                ft_is_open=order["is_open"],
1597
                order_id=order["order_id"],
1598
                status=order["status"],
1599
                average=order["average"],
1600
                cost=order["cost"],
1601
                filled=order["filled"],
1602
                order_date=datetime.strptime(order["order_date"], DATETIME_PRINT_FORMAT),
1603
                order_filled_date=(datetime.fromtimestamp(
1604
                    order["order_filled_timestamp"] // 1000, tz=timezone.utc)
1605
                    if order["order_filled_timestamp"] else None),
1606
                order_type=order["order_type"],
1607
                price=order["price"],
1608
                remaining=order["remaining"],
1609
            )
1610
            trade.orders.append(order_obj)
1✔
1611

1612
        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