2018-03-02 15:22:00 +00:00
|
|
|
"""
|
|
|
|
This module contains the class to persist trades into SQLite
|
|
|
|
"""
|
2017-10-31 23:22:38 +00:00
|
|
|
import logging
|
2021-09-19 23:44:12 +00:00
|
|
|
from datetime import datetime, timedelta, timezone
|
2018-10-21 07:21:32 +00:00
|
|
|
from decimal import Decimal
|
2019-02-25 19:00:17 +00:00
|
|
|
from typing import Any, Dict, List, Optional
|
2017-05-12 17:11:56 +00:00
|
|
|
|
2021-08-03 18:55:22 +00:00
|
|
|
from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
|
2020-09-28 17:39:41 +00:00
|
|
|
create_engine, desc, func, inspect)
|
2018-06-07 19:35:57 +00:00
|
|
|
from sqlalchemy.exc import NoSuchModuleError
|
2021-04-13 15:34:20 +00:00
|
|
|
from sqlalchemy.orm import Query, declarative_base, relationship, scoped_session, sessionmaker
|
2017-11-09 22:45:22 +00:00
|
|
|
from sqlalchemy.pool import StaticPool
|
2020-08-13 14:14:28 +00:00
|
|
|
from sqlalchemy.sql.schema import UniqueConstraint
|
2017-05-12 17:11:56 +00:00
|
|
|
|
2021-08-27 17:54:53 +00:00
|
|
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES
|
2021-08-03 18:55:22 +00:00
|
|
|
from freqtrade.enums import SellType, TradingMode
|
2020-09-06 12:33:45 +00:00
|
|
|
from freqtrade.exceptions import DependencyException, OperationalException
|
2021-08-06 07:15:18 +00:00
|
|
|
from freqtrade.leverage import interest
|
2020-07-15 19:02:31 +00:00
|
|
|
from freqtrade.misc import safe_value_fallback
|
2020-08-13 06:33:46 +00:00
|
|
|
from freqtrade.persistence.migrations import check_migrate
|
2018-06-07 19:35:57 +00:00
|
|
|
|
2020-09-28 17:39:41 +00:00
|
|
|
|
2017-10-31 23:22:38 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
2017-09-08 13:51:00 +00:00
|
|
|
|
2019-09-10 07:42:45 +00:00
|
|
|
|
2018-05-31 19:10:15 +00:00
|
|
|
_DECL_BASE: Any = declarative_base()
|
2018-06-23 13:27:29 +00:00
|
|
|
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
|
2017-05-12 17:11:56 +00:00
|
|
|
|
|
|
|
|
2020-10-16 05:39:12 +00:00
|
|
|
def init_db(db_url: str, clean_open_orders: bool = False) -> None:
|
2017-09-08 13:51:00 +00:00
|
|
|
"""
|
2021-06-20 04:06:51 +00:00
|
|
|
Initializes this module with the given config,
|
|
|
|
registers all known command handlers
|
|
|
|
and starts polling for message updates
|
|
|
|
:param db_url: Database to use
|
|
|
|
:param clean_open_orders: Remove open orders from the database.
|
|
|
|
Useful for dry-run or if all orders have been reset on the exchange.
|
|
|
|
:return: None
|
2017-09-08 13:51:00 +00:00
|
|
|
"""
|
2018-06-07 03:25:53 +00:00
|
|
|
kwargs = {}
|
|
|
|
|
2018-06-07 17:10:26 +00:00
|
|
|
if db_url == 'sqlite://':
|
2018-06-07 03:25:53 +00:00
|
|
|
kwargs.update({
|
|
|
|
'poolclass': StaticPool,
|
2021-05-23 06:56:41 +00:00
|
|
|
})
|
|
|
|
# Take care of thread ownership
|
|
|
|
if db_url.startswith('sqlite://'):
|
|
|
|
kwargs.update({
|
|
|
|
'connect_args': {'check_same_thread': False},
|
2018-06-07 03:25:53 +00:00
|
|
|
})
|
|
|
|
|
2018-06-07 19:35:57 +00:00
|
|
|
try:
|
2021-04-13 15:34:20 +00:00
|
|
|
engine = create_engine(db_url, future=True, **kwargs)
|
2018-06-07 19:35:57 +00:00
|
|
|
except NoSuchModuleError:
|
2019-08-25 18:38:51 +00:00
|
|
|
raise OperationalException(f"Given value for db_url: '{db_url}' "
|
|
|
|
f"is no valid database URL! (See {_SQL_DOCS_URL})")
|
2018-06-07 19:35:57 +00:00
|
|
|
|
2019-10-29 13:26:03 +00:00
|
|
|
# https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope
|
|
|
|
# Scoped sessions proxy requests to the appropriate thread-local session.
|
|
|
|
# We should use the scoped_session object - not a seperately initialized version
|
2021-04-13 15:34:20 +00:00
|
|
|
Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=True))
|
2021-04-05 06:46:12 +00:00
|
|
|
Trade.query = Trade._session.query_property()
|
|
|
|
Order.query = Trade._session.query_property()
|
|
|
|
PairLock.query = Trade._session.query_property()
|
2020-10-17 09:28:34 +00:00
|
|
|
|
2020-08-13 12:50:57 +00:00
|
|
|
previous_tables = inspect(engine).get_table_names()
|
2017-11-07 19:13:36 +00:00
|
|
|
_DECL_BASE.metadata.create_all(engine)
|
2020-08-13 12:50:57 +00:00
|
|
|
check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables)
|
2017-09-08 13:51:00 +00:00
|
|
|
|
2018-06-07 17:10:26 +00:00
|
|
|
# Clean dry_run DB if the db is not in-memory
|
2019-05-30 04:31:34 +00:00
|
|
|
if clean_open_orders and db_url != 'sqlite://':
|
2018-01-23 07:23:29 +00:00
|
|
|
clean_dry_run_db()
|
|
|
|
|
2017-09-08 13:51:00 +00:00
|
|
|
|
2020-10-16 05:39:12 +00:00
|
|
|
def cleanup_db() -> None:
|
2017-10-27 13:52:14 +00:00
|
|
|
"""
|
|
|
|
Flushes all pending operations to disk.
|
|
|
|
:return: None
|
|
|
|
"""
|
2021-04-15 05:57:52 +00:00
|
|
|
Trade.commit()
|
2017-10-27 13:52:14 +00:00
|
|
|
|
|
|
|
|
2018-01-23 07:23:29 +00:00
|
|
|
def clean_dry_run_db() -> None:
|
|
|
|
"""
|
|
|
|
Remove open_order_id from a Dry_run DB
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
|
|
|
# Check we are updating only a dry_run order not a prod one
|
|
|
|
if 'dry_run' in trade.open_order_id:
|
|
|
|
trade.open_order_id = None
|
2021-04-15 05:57:52 +00:00
|
|
|
Trade.commit()
|
2018-01-23 07:23:29 +00:00
|
|
|
|
|
|
|
|
2020-08-13 07:34:53 +00:00
|
|
|
class Order(_DECL_BASE):
|
|
|
|
"""
|
|
|
|
Order database model
|
|
|
|
Keeps a record of all orders placed on the exchange
|
|
|
|
|
|
|
|
One to many relationship with Trades:
|
|
|
|
- One trade can have many orders
|
|
|
|
- One Order can only be associated with one Trade
|
|
|
|
|
|
|
|
Mirrors CCXT Order structure
|
|
|
|
"""
|
|
|
|
__tablename__ = 'orders'
|
2020-08-13 14:14:28 +00:00
|
|
|
# Uniqueness should be ensured over pair, order_id
|
|
|
|
# its likely that order_id is unique per Pair on some exchanges.
|
2020-08-13 15:17:52 +00:00
|
|
|
__table_args__ = (UniqueConstraint('ft_pair', 'order_id', name="_order_pair_order_id"),)
|
2020-08-13 07:34:53 +00:00
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True)
|
2020-08-13 15:17:52 +00:00
|
|
|
ft_trade_id = Column(Integer, ForeignKey('trades.id'), index=True)
|
2020-08-13 07:34:53 +00:00
|
|
|
|
2020-08-13 17:37:41 +00:00
|
|
|
trade = relationship("Trade", back_populates="orders")
|
|
|
|
|
2021-05-22 08:12:23 +00:00
|
|
|
ft_order_side = Column(String(25), nullable=False)
|
|
|
|
ft_pair = Column(String(25), nullable=False)
|
2020-08-13 15:17:52 +00:00
|
|
|
ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
|
2020-08-13 11:39:36 +00:00
|
|
|
|
2021-05-22 08:12:23 +00:00
|
|
|
order_id = Column(String(255), nullable=False, index=True)
|
|
|
|
status = Column(String(255), nullable=True)
|
|
|
|
symbol = Column(String(25), nullable=True)
|
|
|
|
order_type = Column(String(50), nullable=True)
|
|
|
|
side = Column(String(25), nullable=True)
|
2020-08-13 11:39:36 +00:00
|
|
|
price = Column(Float, nullable=True)
|
2021-05-21 18:35:39 +00:00
|
|
|
average = Column(Float, nullable=True)
|
2020-08-13 11:39:36 +00:00
|
|
|
amount = Column(Float, nullable=True)
|
2020-08-13 07:34:53 +00:00
|
|
|
filled = Column(Float, nullable=True)
|
|
|
|
remaining = Column(Float, nullable=True)
|
|
|
|
cost = Column(Float, nullable=True)
|
2020-08-13 12:50:57 +00:00
|
|
|
order_date = Column(DateTime, nullable=True, default=datetime.utcnow)
|
2020-08-13 07:34:53 +00:00
|
|
|
order_filled_date = Column(DateTime, nullable=True)
|
2020-08-13 12:13:58 +00:00
|
|
|
order_update_date = Column(DateTime, nullable=True)
|
2021-06-20 08:25:22 +00:00
|
|
|
|
2021-07-05 05:12:07 +00:00
|
|
|
leverage = Column(Float, nullable=True, default=1.0)
|
2021-07-04 06:11:59 +00:00
|
|
|
|
2020-08-13 12:13:58 +00:00
|
|
|
def __repr__(self):
|
2021-07-04 06:11:59 +00:00
|
|
|
|
2020-08-13 15:17:52 +00:00
|
|
|
return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, '
|
2020-08-24 04:50:43 +00:00
|
|
|
f'side={self.side}, order_type={self.order_type}, status={self.status})')
|
2020-08-13 07:34:53 +00:00
|
|
|
|
2020-08-13 11:39:36 +00:00
|
|
|
def update_from_ccxt_object(self, order):
|
|
|
|
"""
|
|
|
|
Update Order from ccxt response
|
|
|
|
Only updates if fields are available from ccxt -
|
|
|
|
"""
|
|
|
|
if self.order_id != str(order['id']):
|
2020-09-06 12:33:45 +00:00
|
|
|
raise DependencyException("Order-id's don't match")
|
2020-08-13 11:39:36 +00:00
|
|
|
|
|
|
|
self.status = order.get('status', self.status)
|
|
|
|
self.symbol = order.get('symbol', self.symbol)
|
|
|
|
self.order_type = order.get('type', self.order_type)
|
|
|
|
self.side = order.get('side', self.side)
|
|
|
|
self.price = order.get('price', self.price)
|
|
|
|
self.amount = order.get('amount', self.amount)
|
|
|
|
self.filled = order.get('filled', self.filled)
|
2021-05-21 18:35:39 +00:00
|
|
|
self.average = order.get('average', self.average)
|
2020-08-13 11:39:36 +00:00
|
|
|
self.remaining = order.get('remaining', self.remaining)
|
|
|
|
self.cost = order.get('cost', self.cost)
|
2021-06-20 04:06:51 +00:00
|
|
|
self.leverage = order.get('leverage', self.leverage)
|
2021-07-13 01:39:35 +00:00
|
|
|
|
2020-08-13 11:39:36 +00:00
|
|
|
if 'timestamp' in order and order['timestamp'] is not None:
|
2020-08-22 07:12:09 +00:00
|
|
|
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
|
2020-08-13 11:39:36 +00:00
|
|
|
|
2020-09-06 12:17:45 +00:00
|
|
|
self.ft_is_open = True
|
2021-08-27 17:54:53 +00:00
|
|
|
if self.status in NON_OPEN_EXCHANGE_STATES:
|
2020-08-13 15:17:52 +00:00
|
|
|
self.ft_is_open = False
|
2021-08-12 05:02:36 +00:00
|
|
|
if (order.get('filled', 0.0) or 0.0) > 0:
|
2021-04-13 09:55:03 +00:00
|
|
|
self.order_filled_date = datetime.now(timezone.utc)
|
|
|
|
self.order_update_date = datetime.now(timezone.utc)
|
2020-08-13 15:17:52 +00:00
|
|
|
|
2020-08-13 12:13:58 +00:00
|
|
|
@staticmethod
|
2020-08-13 13:39:29 +00:00
|
|
|
def update_orders(orders: List['Order'], order: Dict[str, Any]):
|
2020-08-13 12:13:58 +00:00
|
|
|
"""
|
2020-08-13 15:18:56 +00:00
|
|
|
Get all non-closed orders - useful when trying to batch-update orders
|
2020-08-13 12:13:58 +00:00
|
|
|
"""
|
2021-02-08 18:35:22 +00:00
|
|
|
if not isinstance(order, dict):
|
|
|
|
logger.warning(f"{order} is not a valid response object.")
|
|
|
|
return
|
|
|
|
|
2020-10-20 04:24:46 +00:00
|
|
|
filtered_orders = [o for o in orders if o.order_id == order.get('id')]
|
2020-08-13 13:54:36 +00:00
|
|
|
if filtered_orders:
|
|
|
|
oobj = filtered_orders[0]
|
|
|
|
oobj.update_from_ccxt_object(order)
|
2021-04-13 17:52:33 +00:00
|
|
|
Order.query.session.commit()
|
2020-08-13 13:54:36 +00:00
|
|
|
else:
|
2020-10-20 04:24:46 +00:00
|
|
|
logger.warning(f"Did not find order for {order}.")
|
2020-08-13 12:13:58 +00:00
|
|
|
|
2020-08-13 07:34:53 +00:00
|
|
|
@staticmethod
|
2020-08-13 14:14:28 +00:00
|
|
|
def parse_from_ccxt_object(order: Dict[str, Any], pair: str, side: str) -> 'Order':
|
2020-08-13 07:34:53 +00:00
|
|
|
"""
|
|
|
|
Parse an order from a ccxt object and return a new order Object.
|
|
|
|
"""
|
2020-08-13 14:14:28 +00:00
|
|
|
o = Order(order_id=str(order['id']), ft_order_side=side, ft_pair=pair)
|
2020-08-13 11:39:36 +00:00
|
|
|
|
|
|
|
o.update_from_ccxt_object(order)
|
2020-08-13 07:34:53 +00:00
|
|
|
return o
|
|
|
|
|
2020-08-13 15:18:56 +00:00
|
|
|
@staticmethod
|
2020-08-13 17:37:41 +00:00
|
|
|
def get_open_orders() -> List['Order']:
|
2020-08-13 15:18:56 +00:00
|
|
|
"""
|
|
|
|
"""
|
|
|
|
return Order.query.filter(Order.ft_is_open.is_(True)).all()
|
|
|
|
|
2020-08-13 07:34:53 +00:00
|
|
|
|
2021-02-20 18:29:04 +00:00
|
|
|
class LocalTrade():
|
2018-03-02 15:22:00 +00:00
|
|
|
"""
|
2020-08-13 07:34:53 +00:00
|
|
|
Trade database model.
|
2021-02-20 18:29:04 +00:00
|
|
|
Used in backtesting - must be aligned to Trade model!
|
2017-05-12 17:11:56 +00:00
|
|
|
|
2021-02-20 18:29:04 +00:00
|
|
|
"""
|
|
|
|
use_db: bool = False
|
2020-11-16 19:09:34 +00:00
|
|
|
# Trades container for backtesting
|
2021-02-20 18:29:04 +00:00
|
|
|
trades: List['LocalTrade'] = []
|
2021-03-13 09:16:32 +00:00
|
|
|
trades_open: List['LocalTrade'] = []
|
|
|
|
total_profit: float = 0
|
2021-02-20 18:29:04 +00:00
|
|
|
|
|
|
|
id: int = 0
|
|
|
|
|
|
|
|
orders: List[Order] = []
|
|
|
|
|
|
|
|
exchange: str = ''
|
|
|
|
pair: str = ''
|
|
|
|
is_open: bool = True
|
|
|
|
fee_open: float = 0.0
|
|
|
|
fee_open_cost: Optional[float] = None
|
|
|
|
fee_open_currency: str = ''
|
|
|
|
fee_close: float = 0.0
|
|
|
|
fee_close_cost: Optional[float] = None
|
|
|
|
fee_close_currency: str = ''
|
2021-02-20 19:07:00 +00:00
|
|
|
open_rate: float = 0.0
|
2021-02-20 18:29:04 +00:00
|
|
|
open_rate_requested: Optional[float] = None
|
2020-12-10 18:36:52 +00:00
|
|
|
# open_trade_value - calculated via _calc_open_trade_value
|
2021-02-20 19:07:00 +00:00
|
|
|
open_trade_value: float = 0.0
|
2021-02-20 18:29:04 +00:00
|
|
|
close_rate: Optional[float] = None
|
|
|
|
close_rate_requested: Optional[float] = None
|
|
|
|
close_profit: Optional[float] = None
|
|
|
|
close_profit_abs: Optional[float] = None
|
2021-08-06 07:15:18 +00:00
|
|
|
stake_amount: float = 0.0
|
2021-07-03 15:03:12 +00:00
|
|
|
amount: float = 0.0
|
2021-02-20 18:29:04 +00:00
|
|
|
amount_requested: Optional[float] = None
|
|
|
|
open_date: datetime
|
|
|
|
close_date: Optional[datetime] = None
|
|
|
|
open_order_id: Optional[str] = None
|
2018-06-26 18:49:07 +00:00
|
|
|
# absolute value of the stop loss
|
2021-02-20 18:29:04 +00:00
|
|
|
stop_loss: float = 0.0
|
2019-03-28 20:18:26 +00:00
|
|
|
# percentage value of the stop loss
|
2021-02-20 18:29:04 +00:00
|
|
|
stop_loss_pct: float = 0.0
|
2018-06-26 18:49:07 +00:00
|
|
|
# absolute value of the initial stop loss
|
2021-02-20 18:29:04 +00:00
|
|
|
initial_stop_loss: float = 0.0
|
2019-03-28 20:18:26 +00:00
|
|
|
# percentage value of the initial stop loss
|
2021-02-20 18:29:04 +00:00
|
|
|
initial_stop_loss_pct: float = 0.0
|
2018-11-24 15:53:10 +00:00
|
|
|
# stoploss order id which is on exchange
|
2021-02-20 18:29:04 +00:00
|
|
|
stoploss_order_id: Optional[str] = None
|
2019-01-08 11:39:10 +00:00
|
|
|
# last update time of the stoploss order on exchange
|
2021-02-20 18:29:04 +00:00
|
|
|
stoploss_last_update: Optional[datetime] = None
|
2018-11-24 15:53:10 +00:00
|
|
|
# absolute value of the highest reached price
|
2021-02-20 18:29:04 +00:00
|
|
|
max_rate: float = 0.0
|
2019-03-16 18:54:16 +00:00
|
|
|
# Lowest price reached
|
2021-02-20 18:29:04 +00:00
|
|
|
min_rate: float = 0.0
|
2021-06-20 09:01:03 +00:00
|
|
|
sell_reason: str = ''
|
2021-06-22 03:26:31 +00:00
|
|
|
sell_order_status: str = ''
|
2021-02-20 18:29:04 +00:00
|
|
|
strategy: str = ''
|
2021-07-21 19:34:20 +00:00
|
|
|
buy_tag: Optional[str] = None
|
2021-02-20 18:29:04 +00:00
|
|
|
timeframe: Optional[int] = None
|
2017-05-12 17:11:56 +00:00
|
|
|
|
2021-08-03 18:55:22 +00:00
|
|
|
trading_mode: TradingMode = TradingMode.SPOT
|
|
|
|
|
2021-08-08 22:47:19 +00:00
|
|
|
# Leverage trading properties
|
|
|
|
isolated_liq: Optional[float] = None
|
2021-08-03 18:55:22 +00:00
|
|
|
is_short: bool = False
|
2021-07-05 05:12:07 +00:00
|
|
|
leverage: float = 1.0
|
2021-06-27 09:38:56 +00:00
|
|
|
|
2021-08-08 22:47:19 +00:00
|
|
|
# Margin trading properties
|
|
|
|
interest_rate: float = 0.0
|
2021-06-27 09:38:56 +00:00
|
|
|
|
2021-08-03 18:55:22 +00:00
|
|
|
# Futures properties
|
2021-08-26 05:01:07 +00:00
|
|
|
funding_fees: Optional[float] = None
|
2021-08-03 18:55:22 +00:00
|
|
|
|
2021-07-04 06:11:59 +00:00
|
|
|
@property
|
|
|
|
def has_no_leverage(self) -> bool:
|
2021-07-08 06:33:40 +00:00
|
|
|
"""Returns true if this is a non-leverage, non-short trade"""
|
2021-07-20 23:56:57 +00:00
|
|
|
return ((self.leverage or self.leverage is None) == 1.0 and not self.is_short)
|
2021-06-27 09:38:56 +00:00
|
|
|
|
2021-07-04 06:11:59 +00:00
|
|
|
@property
|
|
|
|
def borrowed(self) -> float:
|
2021-07-08 06:33:40 +00:00
|
|
|
"""
|
|
|
|
The amount of currency borrowed from the exchange for leverage trades
|
|
|
|
If a long trade, the amount is in base currency
|
|
|
|
If a short trade, the amount is in the other currency being traded
|
|
|
|
"""
|
2021-07-04 06:11:59 +00:00
|
|
|
if self.has_no_leverage:
|
|
|
|
return 0.0
|
|
|
|
elif not self.is_short:
|
2021-07-20 23:56:57 +00:00
|
|
|
return (self.amount * self.open_rate) * ((self.leverage-1)/self.leverage)
|
2021-07-04 06:11:59 +00:00
|
|
|
else:
|
|
|
|
return self.amount
|
2021-06-20 04:06:51 +00:00
|
|
|
|
2021-06-27 02:36:19 +00:00
|
|
|
@property
|
|
|
|
def open_date_utc(self):
|
|
|
|
return self.open_date.replace(tzinfo=timezone.utc)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def close_date_utc(self):
|
|
|
|
return self.close_date.replace(tzinfo=timezone.utc)
|
|
|
|
|
2021-07-17 07:57:57 +00:00
|
|
|
@property
|
|
|
|
def enter_side(self) -> str:
|
|
|
|
if self.is_short:
|
|
|
|
return "sell"
|
|
|
|
else:
|
|
|
|
return "buy"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def exit_side(self) -> str:
|
|
|
|
if self.is_short:
|
|
|
|
return "buy"
|
|
|
|
else:
|
|
|
|
return "sell"
|
|
|
|
|
2019-12-17 06:02:02 +00:00
|
|
|
def __init__(self, **kwargs):
|
2021-02-20 18:29:04 +00:00
|
|
|
for key in kwargs:
|
|
|
|
setattr(self, key, kwargs[key])
|
2021-07-25 08:57:17 +00:00
|
|
|
if self.isolated_liq:
|
|
|
|
self.set_isolated_liq(self.isolated_liq)
|
2020-12-10 18:21:20 +00:00
|
|
|
self.recalc_open_trade_value()
|
2021-09-15 05:10:10 +00:00
|
|
|
# TODO-lev: Throw exception if on margin and interest_rate is none
|
2019-12-17 06:02:02 +00:00
|
|
|
|
2021-07-31 06:12:53 +00:00
|
|
|
def _set_stop_loss(self, stop_loss: float, percent: float):
|
2021-07-08 06:33:40 +00:00
|
|
|
"""
|
|
|
|
Method you should use to set self.stop_loss.
|
|
|
|
Assures stop_loss is not passed the liquidation price
|
|
|
|
"""
|
2021-07-25 08:57:17 +00:00
|
|
|
if self.isolated_liq is not None:
|
2021-07-13 01:39:35 +00:00
|
|
|
if self.is_short:
|
2021-07-25 08:57:17 +00:00
|
|
|
sl = min(stop_loss, self.isolated_liq)
|
2021-07-13 01:39:35 +00:00
|
|
|
else:
|
2021-07-25 08:57:17 +00:00
|
|
|
sl = max(stop_loss, self.isolated_liq)
|
2021-07-13 01:39:35 +00:00
|
|
|
else:
|
|
|
|
sl = stop_loss
|
|
|
|
|
|
|
|
if not self.stop_loss:
|
|
|
|
self.initial_stop_loss = sl
|
|
|
|
self.stop_loss = sl
|
2021-07-06 05:53:49 +00:00
|
|
|
|
2021-07-31 06:12:53 +00:00
|
|
|
if self.is_short:
|
|
|
|
self.stop_loss_pct = abs(percent)
|
|
|
|
else:
|
|
|
|
self.stop_loss_pct = -1 * abs(percent)
|
|
|
|
self.stoploss_last_update = datetime.utcnow()
|
|
|
|
|
2021-07-25 08:57:17 +00:00
|
|
|
def set_isolated_liq(self, isolated_liq: float):
|
2021-07-08 06:33:40 +00:00
|
|
|
"""
|
|
|
|
Method you should use to set self.liquidation price.
|
|
|
|
Assures stop_loss is not passed the liquidation price
|
|
|
|
"""
|
2021-07-13 01:39:35 +00:00
|
|
|
if self.stop_loss is not None:
|
|
|
|
if self.is_short:
|
2021-07-25 08:57:17 +00:00
|
|
|
self.stop_loss = min(self.stop_loss, isolated_liq)
|
2021-07-13 01:39:35 +00:00
|
|
|
else:
|
2021-07-25 08:57:17 +00:00
|
|
|
self.stop_loss = max(self.stop_loss, isolated_liq)
|
2021-07-13 01:39:35 +00:00
|
|
|
else:
|
2021-07-25 08:57:17 +00:00
|
|
|
self.initial_stop_loss = isolated_liq
|
|
|
|
self.stop_loss = isolated_liq
|
2021-07-13 01:39:35 +00:00
|
|
|
|
2021-07-25 08:57:17 +00:00
|
|
|
self.isolated_liq = isolated_liq
|
2021-07-06 05:53:49 +00:00
|
|
|
|
2017-05-12 17:11:56 +00:00
|
|
|
def __repr__(self):
|
2020-10-17 09:25:42 +00:00
|
|
|
open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
|
2021-07-27 04:41:45 +00:00
|
|
|
leverage = self.leverage or 1.0
|
|
|
|
is_short = self.is_short or False
|
|
|
|
|
|
|
|
return (
|
|
|
|
f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
|
|
|
|
f'is_short={is_short}, leverage={leverage}, '
|
|
|
|
f'open_rate={self.open_rate:.8f}, open_since={open_since})'
|
|
|
|
)
|
2017-05-12 22:30:08 +00:00
|
|
|
|
2019-05-05 12:07:08 +00:00
|
|
|
def to_json(self) -> Dict[str, Any]:
|
|
|
|
return {
|
|
|
|
'trade_id': self.id,
|
|
|
|
'pair': self.pair,
|
2020-04-06 09:00:31 +00:00
|
|
|
'is_open': self.is_open,
|
2020-06-01 08:53:02 +00:00
|
|
|
'exchange': self.exchange,
|
|
|
|
'amount': round(self.amount, 8),
|
2020-08-12 13:32:56 +00:00
|
|
|
'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
|
2020-06-01 08:53:02 +00:00
|
|
|
'stake_amount': round(self.stake_amount, 8),
|
|
|
|
'strategy': self.strategy,
|
2021-07-21 13:05:35 +00:00
|
|
|
'buy_tag': self.buy_tag,
|
2020-06-02 12:56:34 +00:00
|
|
|
'timeframe': self.timeframe,
|
2020-06-01 08:53:02 +00:00
|
|
|
|
2020-04-06 09:00:31 +00:00
|
|
|
'fee_open': self.fee_open,
|
2020-04-30 04:51:42 +00:00
|
|
|
'fee_open_cost': self.fee_open_cost,
|
|
|
|
'fee_open_currency': self.fee_open_currency,
|
2020-04-06 09:00:31 +00:00
|
|
|
'fee_close': self.fee_close,
|
2020-04-30 04:51:42 +00:00
|
|
|
'fee_close_cost': self.fee_close_cost,
|
|
|
|
'fee_close_currency': self.fee_close_currency,
|
2020-06-01 08:53:02 +00:00
|
|
|
|
2020-10-17 18:32:23 +00:00
|
|
|
'open_date': self.open_date.strftime(DATETIME_PRINT_FORMAT),
|
2020-08-23 08:16:28 +00:00
|
|
|
'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000),
|
2020-06-01 08:53:02 +00:00
|
|
|
'open_rate': self.open_rate,
|
|
|
|
'open_rate_requested': self.open_rate_requested,
|
2020-12-10 18:36:52 +00:00
|
|
|
'open_trade_value': round(self.open_trade_value, 8),
|
2020-06-01 08:53:02 +00:00
|
|
|
|
2020-10-17 18:32:23 +00:00
|
|
|
'close_date': (self.close_date.strftime(DATETIME_PRINT_FORMAT)
|
2019-05-06 04:55:12 +00:00
|
|
|
if self.close_date else None),
|
2020-08-23 08:16:28 +00:00
|
|
|
'close_timestamp': int(self.close_date.replace(
|
|
|
|
tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None,
|
2019-05-05 12:07:08 +00:00
|
|
|
'close_rate': self.close_rate,
|
2020-04-06 09:00:31 +00:00
|
|
|
'close_rate_requested': self.close_rate_requested,
|
2020-11-03 06:34:21 +00:00
|
|
|
'close_profit': self.close_profit, # Deprecated
|
|
|
|
'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
|
|
|
|
'close_profit_abs': self.close_profit_abs, # Deprecated
|
|
|
|
|
2021-02-06 09:30:50 +00:00
|
|
|
'trade_duration_s': (int((self.close_date_utc - self.open_date_utc).total_seconds())
|
2021-01-24 19:09:18 +00:00
|
|
|
if self.close_date else None),
|
2021-02-06 09:30:50 +00:00
|
|
|
'trade_duration': (int((self.close_date_utc - self.open_date_utc).total_seconds() // 60)
|
2021-01-23 11:43:27 +00:00
|
|
|
if self.close_date else None),
|
2021-01-24 19:09:18 +00:00
|
|
|
|
2020-11-03 06:34:21 +00:00
|
|
|
'profit_ratio': self.close_profit,
|
|
|
|
'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
|
|
|
|
'profit_abs': self.close_profit_abs,
|
2020-06-01 08:53:02 +00:00
|
|
|
|
2021-06-20 09:01:03 +00:00
|
|
|
'sell_reason': self.sell_reason,
|
2021-06-22 03:26:31 +00:00
|
|
|
'sell_order_status': self.sell_order_status,
|
2020-06-01 09:05:37 +00:00
|
|
|
'stop_loss_abs': self.stop_loss,
|
2020-06-01 08:53:02 +00:00
|
|
|
'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
|
2019-05-05 12:07:08 +00:00
|
|
|
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
|
2020-05-30 09:34:39 +00:00
|
|
|
'stoploss_order_id': self.stoploss_order_id,
|
2020-10-17 18:32:23 +00:00
|
|
|
'stoploss_last_update': (self.stoploss_last_update.strftime(DATETIME_PRINT_FORMAT)
|
2020-05-30 09:34:39 +00:00
|
|
|
if self.stoploss_last_update else None),
|
2020-08-23 08:16:28 +00:00
|
|
|
'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace(
|
|
|
|
tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None,
|
2020-06-01 09:05:37 +00:00
|
|
|
'initial_stop_loss_abs': self.initial_stop_loss,
|
2020-06-01 08:53:02 +00:00
|
|
|
'initial_stop_loss_ratio': (self.initial_stop_loss_pct
|
|
|
|
if self.initial_stop_loss_pct else None),
|
2019-05-06 04:55:12 +00:00
|
|
|
'initial_stop_loss_pct': (self.initial_stop_loss_pct * 100
|
|
|
|
if self.initial_stop_loss_pct else None),
|
2020-04-06 09:00:31 +00:00
|
|
|
'min_rate': self.min_rate,
|
|
|
|
'max_rate': self.max_rate,
|
2020-06-01 08:53:02 +00:00
|
|
|
|
2021-06-20 04:06:51 +00:00
|
|
|
'leverage': self.leverage,
|
|
|
|
'interest_rate': self.interest_rate,
|
2021-07-25 08:57:17 +00:00
|
|
|
'isolated_liq': self.isolated_liq,
|
2021-06-22 03:26:31 +00:00
|
|
|
'is_short': self.is_short,
|
2021-08-03 18:55:22 +00:00
|
|
|
'trading_mode': self.trading_mode,
|
2021-08-26 05:01:07 +00:00
|
|
|
'funding_fees': self.funding_fees,
|
2020-04-06 09:00:31 +00:00
|
|
|
'open_order_id': self.open_order_id,
|
2019-05-05 12:07:08 +00:00
|
|
|
}
|
|
|
|
|
2020-11-25 08:53:13 +00:00
|
|
|
@staticmethod
|
|
|
|
def reset_trades() -> None:
|
|
|
|
"""
|
|
|
|
Resets all trades. Only active for backtesting mode.
|
|
|
|
"""
|
2021-02-20 18:29:04 +00:00
|
|
|
LocalTrade.trades = []
|
2021-03-13 09:16:32 +00:00
|
|
|
LocalTrade.trades_open = []
|
|
|
|
LocalTrade.total_profit = 0
|
2020-11-25 08:53:13 +00:00
|
|
|
|
2021-08-09 17:38:56 +00:00
|
|
|
def adjust_min_max_rates(self, current_price: float, current_price_low: float) -> None:
|
2019-03-16 18:54:16 +00:00
|
|
|
"""
|
|
|
|
Adjust the max_rate and min_rate.
|
|
|
|
"""
|
2019-03-17 12:12:04 +00:00
|
|
|
self.max_rate = max(current_price, self.max_rate or self.open_rate)
|
2021-08-09 17:38:56 +00:00
|
|
|
self.min_rate = min(current_price_low, self.min_rate or self.open_rate)
|
2019-03-16 18:54:16 +00:00
|
|
|
|
2020-02-02 04:00:40 +00:00
|
|
|
def adjust_stop_loss(self, current_price: float, stoploss: float,
|
|
|
|
initial: bool = False) -> None:
|
2019-03-17 12:18:29 +00:00
|
|
|
"""
|
|
|
|
This adjusts the stop loss to it's most recently observed setting
|
|
|
|
:param current_price: Current rate the asset is traded
|
|
|
|
:param stoploss: Stoploss as factor (sample -0.05 -> -5% below current price).
|
|
|
|
:param initial: Called to initiate stop_loss.
|
|
|
|
Skips everything if self.stop_loss is already set.
|
|
|
|
"""
|
2018-06-27 04:38:49 +00:00
|
|
|
if initial and not (self.stop_loss is None or self.stop_loss == 0):
|
|
|
|
# Don't modify if called with initial and nothing to do
|
|
|
|
return
|
|
|
|
|
2021-06-27 09:38:56 +00:00
|
|
|
if self.is_short:
|
|
|
|
new_loss = float(current_price * (1 + abs(stoploss)))
|
2021-08-08 22:47:19 +00:00
|
|
|
# If trading with leverage, don't set the stoploss below the liquidation price
|
2021-07-25 08:57:17 +00:00
|
|
|
if self.isolated_liq:
|
|
|
|
new_loss = min(self.isolated_liq, new_loss)
|
2021-06-27 09:38:56 +00:00
|
|
|
else:
|
|
|
|
new_loss = float(current_price * (1 - abs(stoploss)))
|
2021-08-08 22:47:19 +00:00
|
|
|
# If trading with leverage, don't set the stoploss below the liquidation price
|
2021-07-25 08:57:17 +00:00
|
|
|
if self.isolated_liq:
|
|
|
|
new_loss = max(self.isolated_liq, new_loss)
|
2018-06-26 18:49:07 +00:00
|
|
|
|
|
|
|
# no stop loss assigned yet
|
2018-07-01 17:54:26 +00:00
|
|
|
if not self.stop_loss:
|
2019-09-11 20:32:08 +00:00
|
|
|
logger.debug(f"{self.pair} - Assigning new stoploss...")
|
2021-07-31 06:12:53 +00:00
|
|
|
self._set_stop_loss(new_loss, stoploss)
|
2018-06-26 18:49:07 +00:00
|
|
|
self.initial_stop_loss = new_loss
|
2021-06-27 09:38:56 +00:00
|
|
|
if self.is_short:
|
|
|
|
self.initial_stop_loss_pct = abs(stoploss)
|
|
|
|
else:
|
|
|
|
self.initial_stop_loss_pct = -1 * abs(stoploss)
|
2018-06-26 18:49:07 +00:00
|
|
|
|
|
|
|
# evaluate if the stop loss needs to be updated
|
|
|
|
else:
|
2021-07-08 03:14:08 +00:00
|
|
|
|
2021-07-08 06:33:40 +00:00
|
|
|
higher_stop = new_loss > self.stop_loss
|
|
|
|
lower_stop = new_loss < self.stop_loss
|
2021-07-08 03:14:08 +00:00
|
|
|
|
|
|
|
# stop losses only walk up, never down!,
|
2021-08-08 22:47:19 +00:00
|
|
|
# ? But adding more to a leveraged trade would create a lower liquidation price,
|
2021-07-08 03:14:08 +00:00
|
|
|
# ? decreasing the minimum stoploss
|
2021-07-08 06:33:40 +00:00
|
|
|
if (higher_stop and not self.is_short) or (lower_stop and self.is_short):
|
2019-09-11 20:32:08 +00:00
|
|
|
logger.debug(f"{self.pair} - Adjusting stoploss...")
|
2021-07-31 06:12:53 +00:00
|
|
|
self._set_stop_loss(new_loss, stoploss)
|
2018-06-26 18:49:07 +00:00
|
|
|
else:
|
2019-09-11 20:32:08 +00:00
|
|
|
logger.debug(f"{self.pair} - Keeping current stoploss...")
|
2018-06-26 18:49:07 +00:00
|
|
|
|
|
|
|
logger.debug(
|
2019-09-11 23:29:47 +00:00
|
|
|
f"{self.pair} - Stoploss adjusted. current_price={current_price:.8f}, "
|
|
|
|
f"open_rate={self.open_rate:.8f}, max_rate={self.max_rate:.8f}, "
|
|
|
|
f"initial_stop_loss={self.initial_stop_loss:.8f}, "
|
|
|
|
f"stop_loss={self.stop_loss:.8f}. "
|
2019-09-10 07:42:45 +00:00
|
|
|
f"Trailing stoploss saved us: "
|
2019-09-11 23:29:47 +00:00
|
|
|
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.")
|
2018-06-26 18:49:07 +00:00
|
|
|
|
2017-10-31 23:22:38 +00:00
|
|
|
def update(self, order: Dict) -> None:
|
2017-06-08 18:01:01 +00:00
|
|
|
"""
|
2017-10-31 23:22:38 +00:00
|
|
|
Updates this entity with amount and actual open/close rates.
|
2020-06-28 14:27:35 +00:00
|
|
|
:param order: order retrieved by exchange.fetch_order()
|
2017-10-31 23:22:38 +00:00
|
|
|
:return: None
|
2017-06-08 18:01:01 +00:00
|
|
|
"""
|
2018-06-23 13:27:29 +00:00
|
|
|
order_type = order['type']
|
2021-06-27 02:36:19 +00:00
|
|
|
|
2021-06-23 04:26:10 +00:00
|
|
|
if 'is_short' in order and order['side'] == 'sell':
|
2021-06-27 06:19:58 +00:00
|
|
|
# Only set's is_short on opening trades, ignores non-shorts
|
2021-06-23 04:26:10 +00:00
|
|
|
self.is_short = order['is_short']
|
2021-06-27 02:36:19 +00:00
|
|
|
|
2017-12-16 01:36:43 +00:00
|
|
|
# Ignore open and cancelled orders
|
2020-07-20 17:39:12 +00:00
|
|
|
if order['status'] == 'open' or safe_value_fallback(order, 'average', 'price') is None:
|
2017-10-31 23:22:38 +00:00
|
|
|
return
|
|
|
|
|
2018-12-27 10:19:26 +00:00
|
|
|
logger.info('Updating trade (id=%s) ...', self.id)
|
2017-12-17 21:07:56 +00:00
|
|
|
|
2021-07-31 06:12:53 +00:00
|
|
|
if order_type in ('market', 'limit') and self.enter_side == order['side']:
|
2017-10-31 23:54:16 +00:00
|
|
|
# Update open rate and actual amount
|
2021-02-20 18:29:04 +00:00
|
|
|
self.open_rate = float(safe_value_fallback(order, 'average', 'price'))
|
|
|
|
self.amount = float(safe_value_fallback(order, 'filled', 'amount'))
|
2021-07-04 06:11:59 +00:00
|
|
|
if 'leverage' in order:
|
2021-06-27 02:36:19 +00:00
|
|
|
self.leverage = order['leverage']
|
2020-12-10 18:21:20 +00:00
|
|
|
self.recalc_open_trade_value()
|
2020-09-19 06:33:12 +00:00
|
|
|
if self.is_open:
|
2021-06-20 08:25:22 +00:00
|
|
|
payment = "SELL" if self.is_short else "BUY"
|
2021-06-22 03:26:31 +00:00
|
|
|
logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.')
|
2017-12-16 00:09:07 +00:00
|
|
|
self.open_order_id = None
|
2021-07-31 06:12:53 +00:00
|
|
|
elif order_type in ('market', 'limit') and self.exit_side == order['side']:
|
2020-09-19 06:33:12 +00:00
|
|
|
if self.is_open:
|
2021-06-20 08:25:22 +00:00
|
|
|
payment = "BUY" if self.is_short else "SELL"
|
2021-08-08 22:47:19 +00:00
|
|
|
# TODO-lev: On shorts, you buy a little bit more than the amount (amount + interest)
|
2021-09-08 06:19:21 +00:00
|
|
|
# TODO-lev: This wll only print the original amount
|
2021-06-22 03:26:31 +00:00
|
|
|
logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.')
|
2021-08-08 22:47:19 +00:00
|
|
|
# TODO-lev: Double check this
|
|
|
|
self.close(safe_value_fallback(order, 'average', 'price'))
|
2020-11-25 20:39:12 +00:00
|
|
|
elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'):
|
2018-11-23 14:17:36 +00:00
|
|
|
self.stoploss_order_id = None
|
2019-03-12 20:46:35 +00:00
|
|
|
self.close_rate_requested = self.stop_loss
|
2021-06-20 09:01:03 +00:00
|
|
|
self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
2020-09-19 06:33:12 +00:00
|
|
|
if self.is_open:
|
|
|
|
logger.info(f'{order_type.upper()} is hit for {self}.')
|
2021-03-09 19:21:08 +00:00
|
|
|
self.close(safe_value_fallback(order, 'average', 'price'))
|
2017-10-31 23:22:38 +00:00
|
|
|
else:
|
2018-06-23 13:27:29 +00:00
|
|
|
raise ValueError(f'Unknown order type: {order_type}')
|
2021-06-13 09:17:44 +00:00
|
|
|
Trade.commit()
|
2017-06-08 18:01:01 +00:00
|
|
|
|
2020-12-07 15:07:00 +00:00
|
|
|
def close(self, rate: float, *, show_msg: bool = True) -> None:
|
2017-12-16 00:09:07 +00:00
|
|
|
"""
|
|
|
|
Sets close_rate to the given rate, calculates total profit
|
|
|
|
and marks trade as closed
|
|
|
|
"""
|
2021-02-20 18:29:04 +00:00
|
|
|
self.close_rate = rate
|
2021-06-20 04:06:51 +00:00
|
|
|
self.close_date = self.close_date or datetime.utcnow()
|
2019-12-17 07:53:30 +00:00
|
|
|
self.close_profit = self.calc_profit_ratio()
|
2020-03-22 10:16:09 +00:00
|
|
|
self.close_profit_abs = self.calc_profit()
|
2017-12-16 00:09:07 +00:00
|
|
|
self.is_open = False
|
2021-06-22 03:26:31 +00:00
|
|
|
self.sell_order_status = 'closed'
|
2017-10-31 23:22:38 +00:00
|
|
|
self.open_order_id = None
|
2020-11-16 19:21:32 +00:00
|
|
|
if show_msg:
|
|
|
|
logger.info(
|
|
|
|
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
|
|
|
|
self
|
|
|
|
)
|
2017-09-08 19:17:58 +00:00
|
|
|
|
2020-05-01 14:00:42 +00:00
|
|
|
def update_fee(self, fee_cost: float, fee_currency: Optional[str], fee_rate: Optional[float],
|
2020-05-01 18:34:58 +00:00
|
|
|
side: str) -> None:
|
2020-05-01 14:00:42 +00:00
|
|
|
"""
|
|
|
|
Update Fee parameters. Only acts once per side
|
|
|
|
"""
|
2021-07-31 06:12:53 +00:00
|
|
|
if self.enter_side == side and self.fee_open_currency is None:
|
2020-05-01 14:00:42 +00:00
|
|
|
self.fee_open_cost = fee_cost
|
|
|
|
self.fee_open_currency = fee_currency
|
|
|
|
if fee_rate is not None:
|
|
|
|
self.fee_open = fee_rate
|
|
|
|
# Assume close-fee will fall into the same fee category and take an educated guess
|
|
|
|
self.fee_close = fee_rate
|
2021-07-31 06:12:53 +00:00
|
|
|
elif self.exit_side == side and self.fee_close_currency is None:
|
2020-05-01 14:00:42 +00:00
|
|
|
self.fee_close_cost = fee_cost
|
|
|
|
self.fee_close_currency = fee_currency
|
|
|
|
if fee_rate is not None:
|
|
|
|
self.fee_close = fee_rate
|
|
|
|
|
2020-05-01 18:34:58 +00:00
|
|
|
def fee_updated(self, side: str) -> bool:
|
2020-05-01 17:54:16 +00:00
|
|
|
"""
|
|
|
|
Verify if this side (buy / sell) has already been updated
|
|
|
|
"""
|
2021-07-31 06:12:53 +00:00
|
|
|
if self.enter_side == side:
|
2020-05-01 17:54:16 +00:00
|
|
|
return self.fee_open_currency is not None
|
2021-07-31 06:12:53 +00:00
|
|
|
elif self.exit_side == side:
|
2020-05-01 17:54:16 +00:00
|
|
|
return self.fee_close_currency is not None
|
2020-05-01 18:02:38 +00:00
|
|
|
else:
|
|
|
|
return False
|
2020-05-01 17:54:16 +00:00
|
|
|
|
2020-08-13 13:39:29 +00:00
|
|
|
def update_order(self, order: Dict) -> None:
|
|
|
|
Order.update_orders(self.orders, order)
|
|
|
|
|
2021-07-11 02:44:57 +00:00
|
|
|
def _calc_open_trade_value(self) -> float:
|
2017-12-17 21:07:56 +00:00
|
|
|
"""
|
2019-12-17 06:09:56 +00:00
|
|
|
Calculate the open_rate including open_fee.
|
2018-11-01 12:05:57 +00:00
|
|
|
:return: Price in of the open trade incl. Fees
|
2017-12-17 21:07:56 +00:00
|
|
|
"""
|
2021-07-11 02:44:57 +00:00
|
|
|
open_trade = Decimal(self.amount) * Decimal(self.open_rate)
|
2021-06-20 04:06:51 +00:00
|
|
|
fees = open_trade * Decimal(self.fee_open)
|
2021-07-08 06:33:40 +00:00
|
|
|
if self.is_short:
|
2021-06-20 08:25:22 +00:00
|
|
|
return float(open_trade - fees)
|
2021-06-20 04:06:51 +00:00
|
|
|
else:
|
2021-06-20 08:25:22 +00:00
|
|
|
return float(open_trade + fees)
|
2017-12-17 21:07:56 +00:00
|
|
|
|
2020-12-10 18:21:20 +00:00
|
|
|
def recalc_open_trade_value(self) -> None:
|
2019-12-17 06:08:36 +00:00
|
|
|
"""
|
2020-12-10 18:21:20 +00:00
|
|
|
Recalculate open_trade_value.
|
2021-07-20 23:56:57 +00:00
|
|
|
Must be called whenever open_rate, fee_open or is_short is changed.
|
|
|
|
|
2019-12-17 06:08:36 +00:00
|
|
|
"""
|
2020-12-10 18:36:52 +00:00
|
|
|
self.open_trade_value = self._calc_open_trade_value()
|
2019-12-17 06:08:36 +00:00
|
|
|
|
2021-06-27 09:38:56 +00:00
|
|
|
def calculate_interest(self, interest_rate: Optional[float] = None) -> Decimal:
|
|
|
|
"""
|
|
|
|
: param interest_rate: interest_charge for borrowing this coin(optional).
|
|
|
|
If interest_rate is not set self.interest_rate will be used
|
|
|
|
"""
|
2021-07-04 06:11:59 +00:00
|
|
|
|
2021-06-27 09:38:56 +00:00
|
|
|
zero = Decimal(0.0)
|
2021-07-04 06:11:59 +00:00
|
|
|
# If nothing was borrowed
|
2021-10-11 13:48:31 +00:00
|
|
|
if self.has_no_leverage or self.trading_mode != TradingMode.MARGIN:
|
2021-06-27 09:38:56 +00:00
|
|
|
return zero
|
|
|
|
|
|
|
|
open_date = self.open_date.replace(tzinfo=None)
|
2021-07-06 06:18:03 +00:00
|
|
|
now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None)
|
2021-06-27 09:38:56 +00:00
|
|
|
sec_per_hour = Decimal(3600)
|
|
|
|
total_seconds = Decimal((now - open_date).total_seconds())
|
|
|
|
hours = total_seconds/sec_per_hour or zero
|
|
|
|
|
|
|
|
rate = Decimal(interest_rate or self.interest_rate)
|
2021-06-23 03:09:52 +00:00
|
|
|
borrowed = Decimal(self.borrowed)
|
2021-07-06 03:48:56 +00:00
|
|
|
|
2021-08-06 07:15:18 +00:00
|
|
|
return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
|
2021-06-23 03:09:52 +00:00
|
|
|
|
2021-08-03 18:55:22 +00:00
|
|
|
def _calc_base_close(self, amount: Decimal, rate: Optional[float] = None,
|
|
|
|
fee: Optional[float] = None) -> Decimal:
|
|
|
|
|
|
|
|
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
|
|
|
|
fees = close_trade * Decimal(fee or self.fee_close)
|
|
|
|
|
|
|
|
if self.is_short:
|
|
|
|
return close_trade + fees
|
|
|
|
else:
|
|
|
|
return close_trade - fees
|
|
|
|
|
2020-12-10 18:21:20 +00:00
|
|
|
def calc_close_trade_value(self, rate: Optional[float] = None,
|
2021-06-27 09:38:56 +00:00
|
|
|
fee: Optional[float] = None,
|
|
|
|
interest_rate: Optional[float] = None) -> float:
|
2017-12-17 21:07:56 +00:00
|
|
|
"""
|
2018-11-01 12:05:57 +00:00
|
|
|
Calculate the close_rate including fee
|
2017-12-17 21:07:56 +00:00
|
|
|
:param fee: fee to use on the close rate (optional).
|
2019-12-17 07:53:30 +00:00
|
|
|
If rate is not set self.fee will be used
|
2017-12-17 21:07:56 +00:00
|
|
|
:param rate: rate to compare with (optional).
|
2019-12-17 07:53:30 +00:00
|
|
|
If rate is not set self.close_rate will be used
|
2021-06-27 09:38:56 +00:00
|
|
|
:param interest_rate: interest_charge for borrowing this coin (optional).
|
|
|
|
If interest_rate is not set self.interest_rate will be used
|
2017-12-17 21:07:56 +00:00
|
|
|
:return: Price in BTC of the open trade
|
|
|
|
"""
|
|
|
|
if rate is None and not self.close_rate:
|
|
|
|
return 0.0
|
|
|
|
|
2021-08-03 18:55:22 +00:00
|
|
|
amount = Decimal(self.amount)
|
|
|
|
trading_mode = self.trading_mode or TradingMode.SPOT
|
2021-06-23 04:26:10 +00:00
|
|
|
|
2021-08-03 18:55:22 +00:00
|
|
|
if trading_mode == TradingMode.SPOT:
|
|
|
|
return float(self._calc_base_close(amount, rate, fee))
|
2021-06-22 03:26:31 +00:00
|
|
|
|
2021-08-03 18:55:22 +00:00
|
|
|
elif (trading_mode == TradingMode.MARGIN):
|
|
|
|
|
|
|
|
total_interest = self.calculate_interest(interest_rate)
|
|
|
|
|
|
|
|
if self.is_short:
|
|
|
|
amount = amount + total_interest
|
|
|
|
return float(self._calc_base_close(amount, rate, fee))
|
|
|
|
else:
|
|
|
|
# Currency already owned for longs, no need to purchase
|
|
|
|
return float(self._calc_base_close(amount, rate, fee) - total_interest)
|
|
|
|
|
|
|
|
elif (trading_mode == TradingMode.FUTURES):
|
2021-08-26 05:01:07 +00:00
|
|
|
funding_fees = self.funding_fees or 0.0
|
2021-10-11 11:56:27 +00:00
|
|
|
if self.is_short:
|
|
|
|
return float(self._calc_base_close(amount, rate, fee)) - funding_fees
|
|
|
|
else:
|
|
|
|
return float(self._calc_base_close(amount, rate, fee)) + funding_fees
|
2021-06-20 04:06:51 +00:00
|
|
|
else:
|
2021-08-03 18:55:22 +00:00
|
|
|
raise OperationalException(
|
|
|
|
f"{self.trading_mode.value} trading is not yet available using freqtrade")
|
2017-12-17 21:07:56 +00:00
|
|
|
|
2019-09-10 07:42:45 +00:00
|
|
|
def calc_profit(self, rate: Optional[float] = None,
|
2021-06-27 09:38:56 +00:00
|
|
|
fee: Optional[float] = None,
|
|
|
|
interest_rate: Optional[float] = None) -> float:
|
2017-12-17 21:07:56 +00:00
|
|
|
"""
|
2018-11-01 12:05:57 +00:00
|
|
|
Calculate the absolute profit in stake currency between Close and Open trade
|
2017-12-17 21:07:56 +00:00
|
|
|
:param fee: fee to use on the close rate (optional).
|
2021-06-20 04:06:51 +00:00
|
|
|
If fee is not set self.fee will be used
|
2017-12-17 21:07:56 +00:00
|
|
|
:param rate: close rate to compare with (optional).
|
2019-12-17 07:53:30 +00:00
|
|
|
If rate is not set self.close_rate will be used
|
2021-06-27 09:38:56 +00:00
|
|
|
:param interest_rate: interest_charge for borrowing this coin (optional).
|
|
|
|
If interest_rate is not set self.interest_rate will be used
|
2018-11-01 12:05:57 +00:00
|
|
|
:return: profit in stake currency as float
|
2017-12-17 21:07:56 +00:00
|
|
|
"""
|
2020-12-10 18:36:52 +00:00
|
|
|
close_trade_value = self.calc_close_trade_value(
|
2018-02-06 17:37:10 +00:00
|
|
|
rate=(rate or self.close_rate),
|
2021-06-27 09:38:56 +00:00
|
|
|
fee=(fee or self.fee_close),
|
|
|
|
interest_rate=(interest_rate or self.interest_rate)
|
2017-12-17 21:07:56 +00:00
|
|
|
)
|
2021-06-20 04:06:51 +00:00
|
|
|
|
2021-06-20 08:25:22 +00:00
|
|
|
if self.is_short:
|
2021-06-20 04:06:51 +00:00
|
|
|
profit = self.open_trade_value - close_trade_value
|
|
|
|
else:
|
|
|
|
profit = close_trade_value - self.open_trade_value
|
2018-06-23 13:27:29 +00:00
|
|
|
return float(f"{profit:.8f}")
|
2017-12-17 21:07:56 +00:00
|
|
|
|
2019-12-17 07:53:30 +00:00
|
|
|
def calc_profit_ratio(self, rate: Optional[float] = None,
|
2021-06-27 09:38:56 +00:00
|
|
|
fee: Optional[float] = None,
|
|
|
|
interest_rate: Optional[float] = None) -> float:
|
2017-10-31 23:22:38 +00:00
|
|
|
"""
|
2019-12-17 07:53:30 +00:00
|
|
|
Calculates the profit as ratio (including fee).
|
2017-10-31 23:22:38 +00:00
|
|
|
:param rate: rate to compare with (optional).
|
2019-12-17 07:53:30 +00:00
|
|
|
If rate is not set self.close_rate will be used
|
2018-03-17 21:12:21 +00:00
|
|
|
:param fee: fee to use on the close rate (optional).
|
2021-06-27 09:38:56 +00:00
|
|
|
:param interest_rate: interest_charge for borrowing this coin (optional).
|
|
|
|
If interest_rate is not set self.interest_rate will be used
|
2019-12-17 07:53:30 +00:00
|
|
|
:return: profit ratio as float
|
2017-10-31 23:22:38 +00:00
|
|
|
"""
|
2020-12-10 18:36:52 +00:00
|
|
|
close_trade_value = self.calc_close_trade_value(
|
2018-02-06 17:37:10 +00:00
|
|
|
rate=(rate or self.close_rate),
|
2021-06-27 09:38:56 +00:00
|
|
|
fee=(fee or self.fee_close),
|
|
|
|
interest_rate=(interest_rate or self.interest_rate)
|
2017-12-17 21:07:56 +00:00
|
|
|
)
|
2021-07-08 11:37:54 +00:00
|
|
|
|
|
|
|
short_close_zero = (self.is_short and close_trade_value == 0.0)
|
|
|
|
long_close_zero = (not self.is_short and self.open_trade_value == 0.0)
|
2021-07-11 02:44:57 +00:00
|
|
|
leverage = self.leverage or 1.0
|
2021-07-08 11:37:54 +00:00
|
|
|
|
|
|
|
if (short_close_zero or long_close_zero):
|
2021-07-02 08:02:00 +00:00
|
|
|
return 0.0
|
2021-06-20 04:06:51 +00:00
|
|
|
else:
|
2021-07-08 11:37:54 +00:00
|
|
|
if self.is_short:
|
2021-07-11 02:44:57 +00:00
|
|
|
profit_ratio = (1 - (close_trade_value/self.open_trade_value)) * leverage
|
2021-07-04 06:11:59 +00:00
|
|
|
else:
|
2021-07-11 02:44:57 +00:00
|
|
|
profit_ratio = ((close_trade_value/self.open_trade_value) - 1) * leverage
|
2021-07-04 06:11:59 +00:00
|
|
|
|
2020-02-28 09:36:39 +00:00
|
|
|
return float(f"{profit_ratio:.8f}")
|
2018-12-03 18:45:00 +00:00
|
|
|
|
2020-09-11 05:12:10 +00:00
|
|
|
def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]:
|
2020-08-22 06:39:10 +00:00
|
|
|
"""
|
2020-09-11 04:59:07 +00:00
|
|
|
Finds latest order for this orderside and status
|
|
|
|
:param order_side: Side of the order (either 'buy' or 'sell')
|
2020-09-11 05:12:10 +00:00
|
|
|
:param is_open: Only search for open orders?
|
2020-09-11 04:59:07 +00:00
|
|
|
:return: latest Order object if it exists, else None
|
2020-08-22 06:39:10 +00:00
|
|
|
"""
|
2020-08-22 13:48:00 +00:00
|
|
|
orders = [o for o in self.orders if o.side == order_side]
|
2020-09-11 05:12:10 +00:00
|
|
|
if is_open is not None:
|
|
|
|
orders = [o for o in orders if o.ft_is_open == is_open]
|
2020-08-22 06:39:10 +00:00
|
|
|
if len(orders) > 0:
|
|
|
|
return orders[-1]
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
2021-06-27 09:38:56 +00:00
|
|
|
@staticmethod
|
2020-11-16 19:09:34 +00:00
|
|
|
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
|
|
|
|
open_date: datetime = None, close_date: datetime = None,
|
2021-02-20 18:29:04 +00:00
|
|
|
) -> List['LocalTrade']:
|
2020-11-16 19:09:34 +00:00
|
|
|
"""
|
|
|
|
Helper function to query Trades.
|
|
|
|
Returns a List of trades, filtered on the parameters given.
|
|
|
|
In live mode, converts the filter to a database query and returns all rows
|
|
|
|
In Backtest mode, uses filters on Trade.trades to get the result.
|
|
|
|
|
|
|
|
:return: unsorted List[Trade]
|
|
|
|
"""
|
2021-02-20 18:29:04 +00:00
|
|
|
|
|
|
|
# Offline mode - without database
|
2021-03-13 09:16:32 +00:00
|
|
|
if is_open is not None:
|
|
|
|
if is_open:
|
|
|
|
sel_trades = LocalTrade.trades_open
|
|
|
|
else:
|
|
|
|
sel_trades = LocalTrade.trades
|
|
|
|
|
|
|
|
else:
|
|
|
|
# Not used during backtesting, but might be used by a strategy
|
2021-03-21 11:44:31 +00:00
|
|
|
sel_trades = list(LocalTrade.trades + LocalTrade.trades_open)
|
2021-03-13 09:16:32 +00:00
|
|
|
|
2021-02-20 18:29:04 +00:00
|
|
|
if pair:
|
|
|
|
sel_trades = [trade for trade in sel_trades if trade.pair == pair]
|
|
|
|
if open_date:
|
|
|
|
sel_trades = [trade for trade in sel_trades if trade.open_date > open_date]
|
|
|
|
if close_date:
|
|
|
|
sel_trades = [trade for trade in sel_trades if trade.close_date
|
|
|
|
and trade.close_date > close_date]
|
2021-03-13 09:16:32 +00:00
|
|
|
|
2021-06-22 03:26:31 +00:00
|
|
|
return sel_trades
|
2020-11-16 19:09:34 +00:00
|
|
|
|
2021-06-27 09:38:56 +00:00
|
|
|
@staticmethod
|
2021-03-13 09:16:32 +00:00
|
|
|
def close_bt_trade(trade):
|
|
|
|
LocalTrade.trades_open.remove(trade)
|
|
|
|
LocalTrade.trades.append(trade)
|
|
|
|
LocalTrade.total_profit += trade.close_profit_abs
|
|
|
|
|
2021-06-27 09:38:56 +00:00
|
|
|
@staticmethod
|
2021-03-13 09:16:32 +00:00
|
|
|
def add_bt_trade(trade):
|
|
|
|
if trade.is_open:
|
|
|
|
LocalTrade.trades_open.append(trade)
|
|
|
|
else:
|
|
|
|
LocalTrade.trades.append(trade)
|
|
|
|
|
2021-06-27 09:38:56 +00:00
|
|
|
@staticmethod
|
2019-10-29 14:01:10 +00:00
|
|
|
def get_open_trades() -> List[Any]:
|
|
|
|
"""
|
|
|
|
Query trades from persistence layer
|
|
|
|
"""
|
2021-04-27 18:25:36 +00:00
|
|
|
return Trade.get_trades_proxy(is_open=True)
|
2019-10-29 10:15:33 +00:00
|
|
|
|
2021-06-27 09:38:56 +00:00
|
|
|
@staticmethod
|
2019-05-20 05:06:40 +00:00
|
|
|
def stoploss_reinitialization(desired_stoploss):
|
|
|
|
"""
|
|
|
|
Adjust initial Stoploss to desired stoploss for all open trades.
|
|
|
|
"""
|
|
|
|
for trade in Trade.get_open_trades():
|
|
|
|
logger.info("Found open trade: %s", trade)
|
|
|
|
|
|
|
|
# skip case if trailing-stop changed the stoploss already.
|
|
|
|
if (trade.stop_loss == trade.initial_stop_loss
|
2021-07-21 13:05:35 +00:00
|
|
|
and trade.initial_stop_loss_pct != desired_stoploss):
|
2019-05-20 05:06:40 +00:00
|
|
|
# Stoploss value got changed
|
|
|
|
|
2019-09-10 07:42:45 +00:00
|
|
|
logger.info(f"Stoploss for {trade} needs adjustment...")
|
2019-05-20 05:06:40 +00:00
|
|
|
# Force reset of stoploss
|
|
|
|
trade.stop_loss = None
|
|
|
|
trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
|
2019-09-10 07:42:45 +00:00
|
|
|
logger.info(f"New stoploss: {trade.stop_loss}.")
|
2020-10-17 09:28:34 +00:00
|
|
|
|
|
|
|
|
2021-02-20 18:29:04 +00:00
|
|
|
class Trade(_DECL_BASE, LocalTrade):
|
|
|
|
"""
|
|
|
|
Trade database model.
|
|
|
|
Also handles updating and querying trades
|
2021-02-27 18:57:42 +00:00
|
|
|
|
|
|
|
Note: Fields must be aligned with LocalTrade class
|
2021-02-20 18:29:04 +00:00
|
|
|
"""
|
|
|
|
__tablename__ = 'trades'
|
|
|
|
|
|
|
|
use_db: bool = True
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
|
|
|
|
|
|
orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan")
|
|
|
|
|
2021-05-22 08:12:23 +00:00
|
|
|
exchange = Column(String(25), nullable=False)
|
|
|
|
pair = Column(String(25), nullable=False, index=True)
|
2021-02-20 18:29:04 +00:00
|
|
|
is_open = Column(Boolean, nullable=False, default=True, index=True)
|
|
|
|
fee_open = Column(Float, nullable=False, default=0.0)
|
|
|
|
fee_open_cost = Column(Float, nullable=True)
|
2021-05-22 08:12:23 +00:00
|
|
|
fee_open_currency = Column(String(25), nullable=True)
|
2021-02-20 18:29:04 +00:00
|
|
|
fee_close = Column(Float, nullable=False, default=0.0)
|
|
|
|
fee_close_cost = Column(Float, nullable=True)
|
2021-05-22 08:12:23 +00:00
|
|
|
fee_close_currency = Column(String(25), nullable=True)
|
2021-02-20 18:29:04 +00:00
|
|
|
open_rate = Column(Float)
|
|
|
|
open_rate_requested = Column(Float)
|
|
|
|
# open_trade_value - calculated via _calc_open_trade_value
|
|
|
|
open_trade_value = Column(Float)
|
|
|
|
close_rate = Column(Float)
|
|
|
|
close_rate_requested = Column(Float)
|
|
|
|
close_profit = Column(Float)
|
|
|
|
close_profit_abs = Column(Float)
|
|
|
|
stake_amount = Column(Float, nullable=False)
|
2021-07-03 15:03:12 +00:00
|
|
|
amount = Column(Float)
|
2021-02-20 18:29:04 +00:00
|
|
|
amount_requested = Column(Float)
|
|
|
|
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
|
|
|
close_date = Column(DateTime)
|
2021-05-22 08:12:23 +00:00
|
|
|
open_order_id = Column(String(255))
|
2021-02-20 18:29:04 +00:00
|
|
|
# absolute value of the stop loss
|
|
|
|
stop_loss = Column(Float, nullable=True, default=0.0)
|
|
|
|
# percentage value of the stop loss
|
|
|
|
stop_loss_pct = Column(Float, nullable=True)
|
|
|
|
# absolute value of the initial stop loss
|
|
|
|
initial_stop_loss = Column(Float, nullable=True, default=0.0)
|
|
|
|
# percentage value of the initial stop loss
|
|
|
|
initial_stop_loss_pct = Column(Float, nullable=True)
|
|
|
|
# stoploss order id which is on exchange
|
2021-05-22 08:12:23 +00:00
|
|
|
stoploss_order_id = Column(String(255), nullable=True, index=True)
|
2021-02-20 18:29:04 +00:00
|
|
|
# last update time of the stoploss order on exchange
|
|
|
|
stoploss_last_update = Column(DateTime, nullable=True)
|
|
|
|
# absolute value of the highest reached price
|
|
|
|
max_rate = Column(Float, nullable=True, default=0.0)
|
|
|
|
# Lowest price reached
|
|
|
|
min_rate = Column(Float, nullable=True)
|
2021-08-08 22:47:19 +00:00
|
|
|
sell_reason = Column(String(100), nullable=True) # TODO-lev: Change to close_reason
|
|
|
|
sell_order_status = Column(String(100), nullable=True) # TODO-lev: Change to close_order_status
|
2021-05-22 08:12:23 +00:00
|
|
|
strategy = Column(String(100), nullable=True)
|
2021-07-21 13:05:35 +00:00
|
|
|
buy_tag = Column(String(100), nullable=True)
|
2021-02-20 18:29:04 +00:00
|
|
|
timeframe = Column(Integer, nullable=True)
|
|
|
|
|
2021-10-13 02:34:19 +00:00
|
|
|
trading_mode = Column(Enum(TradingMode), nullable=True)
|
2021-08-03 18:55:22 +00:00
|
|
|
|
2021-08-08 22:47:19 +00:00
|
|
|
# Leverage trading properties
|
2021-07-05 05:12:07 +00:00
|
|
|
leverage = Column(Float, nullable=True, default=1.0)
|
2021-06-20 08:25:22 +00:00
|
|
|
is_short = Column(Boolean, nullable=False, default=False)
|
2021-08-08 22:47:19 +00:00
|
|
|
isolated_liq = Column(Float, nullable=True)
|
|
|
|
|
|
|
|
# Margin Trading Properties
|
|
|
|
interest_rate = Column(Float, nullable=False, default=0.0)
|
2021-06-20 04:19:09 +00:00
|
|
|
|
2021-08-03 18:55:22 +00:00
|
|
|
# Futures properties
|
2021-08-26 05:01:07 +00:00
|
|
|
funding_fees = Column(Float, nullable=True, default=None)
|
2021-08-03 18:55:22 +00:00
|
|
|
|
2021-02-20 18:29:04 +00:00
|
|
|
def __init__(self, **kwargs):
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
self.recalc_open_trade_value()
|
|
|
|
|
|
|
|
def delete(self) -> None:
|
|
|
|
|
|
|
|
for order in self.orders:
|
2021-04-05 05:28:51 +00:00
|
|
|
Order.query.session.delete(order)
|
2021-02-20 18:29:04 +00:00
|
|
|
|
2021-04-05 05:28:51 +00:00
|
|
|
Trade.query.session.delete(self)
|
2021-04-15 05:57:52 +00:00
|
|
|
Trade.commit()
|
|
|
|
|
2021-06-27 09:38:56 +00:00
|
|
|
@staticmethod
|
2021-04-15 05:57:52 +00:00
|
|
|
def commit():
|
2021-04-13 17:52:33 +00:00
|
|
|
Trade.query.session.commit()
|
2021-02-20 18:29:04 +00:00
|
|
|
|
2021-06-27 09:38:56 +00:00
|
|
|
@staticmethod
|
2021-02-20 18:29:04 +00:00
|
|
|
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
|
|
|
|
open_date: datetime = None, close_date: datetime = None,
|
|
|
|
) -> List['LocalTrade']:
|
|
|
|
"""
|
2021-04-27 18:25:36 +00:00
|
|
|
Helper function to query Trades.j
|
2021-02-20 18:29:04 +00:00
|
|
|
Returns a List of trades, filtered on the parameters given.
|
|
|
|
In live mode, converts the filter to a database query and returns all rows
|
|
|
|
In Backtest mode, uses filters on Trade.trades to get the result.
|
|
|
|
|
|
|
|
:return: unsorted List[Trade]
|
|
|
|
"""
|
|
|
|
if Trade.use_db:
|
|
|
|
trade_filter = []
|
|
|
|
if pair:
|
|
|
|
trade_filter.append(Trade.pair == pair)
|
|
|
|
if open_date:
|
|
|
|
trade_filter.append(Trade.open_date > open_date)
|
|
|
|
if close_date:
|
|
|
|
trade_filter.append(Trade.close_date > close_date)
|
|
|
|
if is_open is not None:
|
|
|
|
trade_filter.append(Trade.is_open.is_(is_open))
|
|
|
|
return Trade.get_trades(trade_filter).all()
|
|
|
|
else:
|
|
|
|
return LocalTrade.get_trades_proxy(
|
|
|
|
pair=pair, is_open=is_open,
|
|
|
|
open_date=open_date,
|
|
|
|
close_date=close_date
|
|
|
|
)
|
|
|
|
|
2021-06-27 09:38:56 +00:00
|
|
|
@staticmethod
|
2021-04-27 18:25:36 +00:00
|
|
|
def get_trades(trade_filter=None) -> Query:
|
|
|
|
"""
|
|
|
|
Helper function to query Trades using filters.
|
|
|
|
NOTE: Not supported in Backtesting.
|
|
|
|
:param trade_filter: Optional filter to apply to trades
|
|
|
|
Can be either a Filter object, or a List of filters
|
|
|
|
e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])`
|
|
|
|
e.g. `(trade_filter=Trade.id == trade_id)`
|
|
|
|
:return: unsorted query object
|
|
|
|
"""
|
|
|
|
if not Trade.use_db:
|
|
|
|
raise NotImplementedError('`Trade.get_trades()` not supported in backtesting mode.')
|
|
|
|
if trade_filter is not None:
|
|
|
|
if not isinstance(trade_filter, list):
|
|
|
|
trade_filter = [trade_filter]
|
|
|
|
return Trade.query.filter(*trade_filter)
|
|
|
|
else:
|
|
|
|
return Trade.query
|
|
|
|
|
2021-06-27 09:38:56 +00:00
|
|
|
@staticmethod
|
2021-04-27 18:25:36 +00:00
|
|
|
def get_open_order_trades():
|
|
|
|
"""
|
|
|
|
Returns all open trades
|
|
|
|
NOTE: Not supported in Backtesting.
|
|
|
|
"""
|
|
|
|
return Trade.get_trades(Trade.open_order_id.isnot(None)).all()
|
|
|
|
|
2021-06-27 09:38:56 +00:00
|
|
|
@staticmethod
|
2021-04-27 18:25:36 +00:00
|
|
|
def get_open_trades_without_assigned_fees():
|
|
|
|
"""
|
|
|
|
Returns all open trades which don't have open fees set correctly
|
|
|
|
NOTE: Not supported in Backtesting.
|
|
|
|
"""
|
|
|
|
return Trade.get_trades([Trade.fee_open_currency.is_(None),
|
|
|
|
Trade.orders.any(),
|
|
|
|
Trade.is_open.is_(True),
|
|
|
|
]).all()
|
|
|
|
|
2021-06-27 09:38:56 +00:00
|
|
|
@staticmethod
|
2021-06-20 04:19:09 +00:00
|
|
|
def get_closed_trades_without_assigned_fees():
|
2021-04-27 18:25:36 +00:00
|
|
|
"""
|
|
|
|
Returns all closed trades which don't have fees set correctly
|
|
|
|
NOTE: Not supported in Backtesting.
|
|
|
|
"""
|
|
|
|
return Trade.get_trades([Trade.fee_close_currency.is_(None),
|
|
|
|
Trade.orders.any(),
|
|
|
|
Trade.is_open.is_(False),
|
|
|
|
]).all()
|
|
|
|
|
2021-07-10 10:18:55 +00:00
|
|
|
@staticmethod
|
|
|
|
def get_total_closed_profit() -> float:
|
|
|
|
"""
|
|
|
|
Retrieves total realized profit
|
|
|
|
"""
|
|
|
|
if Trade.use_db:
|
|
|
|
total_profit = Trade.query.with_entities(
|
|
|
|
func.sum(Trade.close_profit_abs)).filter(Trade.is_open.is_(False)).scalar()
|
|
|
|
else:
|
|
|
|
total_profit = sum(
|
|
|
|
t.close_profit_abs for t in LocalTrade.get_trades_proxy(is_open=False))
|
|
|
|
return total_profit or 0
|
|
|
|
|
2021-04-27 18:25:36 +00:00
|
|
|
@staticmethod
|
|
|
|
def total_open_trades_stakes() -> float:
|
|
|
|
"""
|
|
|
|
Calculates total invested amount in open trades
|
|
|
|
in stake currency
|
|
|
|
"""
|
|
|
|
if Trade.use_db:
|
|
|
|
total_open_stake_amount = Trade.query.with_entities(
|
|
|
|
func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar()
|
|
|
|
else:
|
|
|
|
total_open_stake_amount = sum(
|
|
|
|
t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True))
|
|
|
|
return total_open_stake_amount or 0
|
|
|
|
|
2021-06-27 09:38:56 +00:00
|
|
|
@staticmethod
|
2021-09-19 23:44:12 +00:00
|
|
|
def get_overall_performance(minutes=None) -> List[Dict[str, Any]]:
|
2021-04-27 18:25:36 +00:00
|
|
|
"""
|
|
|
|
Returns List of dicts containing all Trades, including profit and trade count
|
|
|
|
NOTE: Not supported in Backtesting.
|
|
|
|
"""
|
2021-09-19 23:44:12 +00:00
|
|
|
filters = [Trade.is_open.is_(False)]
|
|
|
|
if minutes:
|
|
|
|
start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes)
|
|
|
|
filters.append(Trade.close_date >= start_date)
|
2021-04-27 18:25:36 +00:00
|
|
|
pair_rates = Trade.query.with_entities(
|
|
|
|
Trade.pair,
|
|
|
|
func.sum(Trade.close_profit).label('profit_sum'),
|
2021-05-15 17:39:46 +00:00
|
|
|
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
2021-04-27 18:25:36 +00:00
|
|
|
func.count(Trade.pair).label('count')
|
2021-09-19 23:44:12 +00:00
|
|
|
).filter(*filters)\
|
2021-04-27 18:25:36 +00:00
|
|
|
.group_by(Trade.pair) \
|
2021-05-15 17:39:46 +00:00
|
|
|
.order_by(desc('profit_sum_abs')) \
|
2021-04-27 18:25:36 +00:00
|
|
|
.all()
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
'pair': pair,
|
2021-10-31 09:42:42 +00:00
|
|
|
'profit_ratio': profit,
|
|
|
|
'profit': round(profit * 100, 2), # Compatibility mode
|
|
|
|
'profit_pct': round(profit * 100, 2),
|
2021-05-15 17:39:46 +00:00
|
|
|
'profit_abs': profit_abs,
|
2021-04-27 18:25:36 +00:00
|
|
|
'count': count
|
|
|
|
}
|
2021-05-15 17:39:46 +00:00
|
|
|
for pair, profit, profit_abs, count in pair_rates
|
2021-04-27 18:25:36 +00:00
|
|
|
]
|
|
|
|
|
2021-10-12 21:02:28 +00:00
|
|
|
@staticmethod
|
2021-10-24 13:18:29 +00:00
|
|
|
def get_buy_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
2021-10-12 21:02:28 +00:00
|
|
|
"""
|
|
|
|
Returns List of dicts containing all Trades, based on buy tag performance
|
|
|
|
Can either be average for all pairs or a specific pair provided
|
|
|
|
NOTE: Not supported in Backtesting.
|
|
|
|
"""
|
|
|
|
|
2021-10-25 20:43:22 +00:00
|
|
|
filters = [Trade.is_open.is_(False)]
|
2021-10-12 21:02:28 +00:00
|
|
|
if(pair is not None):
|
2021-10-25 20:43:22 +00:00
|
|
|
filters.append(Trade.pair == pair)
|
|
|
|
|
|
|
|
buy_tag_perf = Trade.query.with_entities(
|
|
|
|
Trade.buy_tag,
|
|
|
|
func.sum(Trade.close_profit).label('profit_sum'),
|
|
|
|
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
|
|
|
func.count(Trade.pair).label('count')
|
|
|
|
).filter(*filters)\
|
|
|
|
.group_by(Trade.buy_tag) \
|
|
|
|
.order_by(desc('profit_sum_abs')) \
|
|
|
|
.all()
|
|
|
|
|
2021-10-31 09:42:42 +00:00
|
|
|
return [
|
2021-10-12 21:02:28 +00:00
|
|
|
{
|
2021-10-18 20:56:41 +00:00
|
|
|
'buy_tag': buy_tag if buy_tag is not None else "Other",
|
2021-10-31 09:42:42 +00:00
|
|
|
'profit_ratio': profit,
|
|
|
|
'profit_pct': round(profit * 100, 2),
|
2021-10-12 21:02:28 +00:00
|
|
|
'profit_abs': profit_abs,
|
|
|
|
'count': count
|
|
|
|
}
|
2021-10-25 20:43:22 +00:00
|
|
|
for buy_tag, profit, profit_abs, count in buy_tag_perf
|
2021-10-12 21:02:28 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
@staticmethod
|
2021-10-24 13:18:29 +00:00
|
|
|
def get_sell_reason_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
2021-10-12 21:02:28 +00:00
|
|
|
"""
|
2021-10-19 22:26:15 +00:00
|
|
|
Returns List of dicts containing all Trades, based on sell reason performance
|
2021-10-12 21:02:28 +00:00
|
|
|
Can either be average for all pairs or a specific pair provided
|
|
|
|
NOTE: Not supported in Backtesting.
|
|
|
|
"""
|
2021-10-25 20:43:22 +00:00
|
|
|
|
|
|
|
filters = [Trade.is_open.is_(False)]
|
2021-10-12 21:02:28 +00:00
|
|
|
if(pair is not None):
|
2021-10-25 20:43:22 +00:00
|
|
|
filters.append(Trade.pair == pair)
|
|
|
|
|
|
|
|
sell_tag_perf = Trade.query.with_entities(
|
|
|
|
Trade.sell_reason,
|
|
|
|
func.sum(Trade.close_profit).label('profit_sum'),
|
|
|
|
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
|
|
|
func.count(Trade.pair).label('count')
|
|
|
|
).filter(*filters)\
|
|
|
|
.group_by(Trade.sell_reason) \
|
|
|
|
.order_by(desc('profit_sum_abs')) \
|
|
|
|
.all()
|
|
|
|
|
2021-10-31 09:42:42 +00:00
|
|
|
return [
|
2021-10-12 21:02:28 +00:00
|
|
|
{
|
2021-10-19 22:26:15 +00:00
|
|
|
'sell_reason': sell_reason if sell_reason is not None else "Other",
|
2021-10-31 09:42:42 +00:00
|
|
|
'profit_ratio': profit,
|
|
|
|
'profit_pct': round(profit * 100, 2),
|
2021-10-12 21:02:28 +00:00
|
|
|
'profit_abs': profit_abs,
|
|
|
|
'count': count
|
|
|
|
}
|
2021-10-25 20:43:22 +00:00
|
|
|
for sell_reason, profit, profit_abs, count in sell_tag_perf
|
2021-10-12 21:02:28 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
@staticmethod
|
2021-10-24 13:18:29 +00:00
|
|
|
def get_mix_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
2021-10-12 21:02:28 +00:00
|
|
|
"""
|
2021-10-19 22:26:15 +00:00
|
|
|
Returns List of dicts containing all Trades, based on buy_tag + sell_reason performance
|
2021-10-12 21:02:28 +00:00
|
|
|
Can either be average for all pairs or a specific pair provided
|
|
|
|
NOTE: Not supported in Backtesting.
|
|
|
|
"""
|
2021-10-25 20:43:22 +00:00
|
|
|
|
|
|
|
filters = [Trade.is_open.is_(False)]
|
2021-10-12 21:02:28 +00:00
|
|
|
if(pair is not None):
|
2021-10-25 20:43:22 +00:00
|
|
|
filters.append(Trade.pair == pair)
|
2021-10-18 20:56:41 +00:00
|
|
|
|
2021-10-25 20:43:22 +00:00
|
|
|
mix_tag_perf = Trade.query.with_entities(
|
|
|
|
Trade.id,
|
|
|
|
Trade.buy_tag,
|
|
|
|
Trade.sell_reason,
|
|
|
|
func.sum(Trade.close_profit).label('profit_sum'),
|
|
|
|
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
|
|
|
func.count(Trade.pair).label('count')
|
|
|
|
).filter(*filters)\
|
|
|
|
.group_by(Trade.id) \
|
|
|
|
.order_by(desc('profit_sum_abs')) \
|
|
|
|
.all()
|
2021-10-18 20:56:41 +00:00
|
|
|
|
2021-10-20 17:13:34 +00:00
|
|
|
return_list: List[Dict] = []
|
2021-10-25 20:43:22 +00:00
|
|
|
for id, buy_tag, sell_reason, profit, profit_abs, count in mix_tag_perf:
|
2021-10-18 20:56:41 +00:00
|
|
|
buy_tag = buy_tag if buy_tag is not None else "Other"
|
2021-10-19 22:26:15 +00:00
|
|
|
sell_reason = sell_reason if sell_reason is not None else "Other"
|
2021-10-18 20:56:41 +00:00
|
|
|
|
2021-10-19 22:26:15 +00:00
|
|
|
if(sell_reason is not None and buy_tag is not None):
|
|
|
|
mix_tag = buy_tag + " " + sell_reason
|
2021-10-18 20:56:41 +00:00
|
|
|
i = 0
|
|
|
|
if not any(item["mix_tag"] == mix_tag for item in return_list):
|
|
|
|
return_list.append({'mix_tag': mix_tag,
|
|
|
|
'profit': profit,
|
|
|
|
'profit_abs': profit_abs,
|
|
|
|
'count': count})
|
|
|
|
else:
|
|
|
|
while i < len(return_list):
|
|
|
|
if return_list[i]["mix_tag"] == mix_tag:
|
|
|
|
return_list[i] = {
|
|
|
|
'mix_tag': mix_tag,
|
|
|
|
'profit': profit + return_list[i]["profit"],
|
|
|
|
'profit_abs': profit_abs + return_list[i]["profit_abs"],
|
|
|
|
'count': 1 + return_list[i]["count"]}
|
|
|
|
i += 1
|
|
|
|
|
2021-10-25 20:43:22 +00:00
|
|
|
[x.update({'profit': round(x['profit'] * 100, 2)}) for x in return_list]
|
2021-10-18 20:56:41 +00:00
|
|
|
return return_list
|
2021-10-12 21:02:28 +00:00
|
|
|
|
2021-04-27 18:25:36 +00:00
|
|
|
@staticmethod
|
2021-06-28 20:42:09 +00:00
|
|
|
def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)):
|
2021-04-27 18:25:36 +00:00
|
|
|
"""
|
|
|
|
Get best pair with closed trade.
|
|
|
|
NOTE: Not supported in Backtesting.
|
|
|
|
:returns: Tuple containing (pair, profit_sum)
|
|
|
|
"""
|
|
|
|
best_pair = Trade.query.with_entities(
|
|
|
|
Trade.pair, func.sum(Trade.close_profit).label('profit_sum')
|
2021-06-28 20:42:09 +00:00
|
|
|
).filter(Trade.is_open.is_(False) & (Trade.close_date >= start_date)) \
|
2021-04-27 18:25:36 +00:00
|
|
|
.group_by(Trade.pair) \
|
|
|
|
.order_by(desc('profit_sum')).first()
|
|
|
|
return best_pair
|
|
|
|
|
2021-02-20 18:29:04 +00:00
|
|
|
|
2020-10-17 09:28:34 +00:00
|
|
|
class PairLock(_DECL_BASE):
|
|
|
|
"""
|
|
|
|
Pair Locks database model.
|
|
|
|
"""
|
2020-10-22 05:35:25 +00:00
|
|
|
__tablename__ = 'pairlocks'
|
2020-10-17 09:28:34 +00:00
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
|
|
|
2021-05-22 08:12:23 +00:00
|
|
|
pair = Column(String(25), nullable=False, index=True)
|
|
|
|
reason = Column(String(255), nullable=True)
|
2020-10-17 09:28:34 +00:00
|
|
|
# Time the pair was locked (start time)
|
|
|
|
lock_time = Column(DateTime, nullable=False)
|
|
|
|
# Time until the pair is locked (end time)
|
2020-10-22 05:35:25 +00:00
|
|
|
lock_end_time = Column(DateTime, nullable=False, index=True)
|
2020-10-17 09:28:34 +00:00
|
|
|
|
2020-10-17 13:15:35 +00:00
|
|
|
active = Column(Boolean, nullable=False, default=True, index=True)
|
2020-10-17 09:28:34 +00:00
|
|
|
|
|
|
|
def __repr__(self):
|
2020-10-17 09:40:01 +00:00
|
|
|
lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT)
|
|
|
|
lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT)
|
2020-10-17 09:28:34 +00:00
|
|
|
return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, '
|
2021-10-28 13:16:07 +00:00
|
|
|
f'lock_end_time={lock_end_time}, reason={self.reason}, active={self.active})')
|
2020-10-17 09:28:34 +00:00
|
|
|
|
2021-06-27 09:38:56 +00:00
|
|
|
@staticmethod
|
2020-10-27 09:08:24 +00:00
|
|
|
def query_pair_locks(pair: Optional[str], now: datetime) -> Query:
|
2020-10-17 09:28:34 +00:00
|
|
|
"""
|
2020-10-24 11:55:54 +00:00
|
|
|
Get all currently active locks for this pair
|
2020-10-17 13:15:35 +00:00
|
|
|
:param pair: Pair to check for. Returns all current locks if pair is empty
|
|
|
|
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
2020-10-17 09:28:34 +00:00
|
|
|
"""
|
2020-10-26 06:37:07 +00:00
|
|
|
filters = [PairLock.lock_end_time > now,
|
2020-10-17 13:15:35 +00:00
|
|
|
# Only active locks
|
|
|
|
PairLock.active.is_(True), ]
|
|
|
|
if pair:
|
|
|
|
filters.append(PairLock.pair == pair)
|
2020-10-17 09:28:34 +00:00
|
|
|
return PairLock.query.filter(
|
2020-10-17 13:15:35 +00:00
|
|
|
*filters
|
2020-10-25 09:54:30 +00:00
|
|
|
)
|
2020-10-17 13:15:35 +00:00
|
|
|
|
|
|
|
def to_json(self) -> Dict[str, Any]:
|
|
|
|
return {
|
2021-03-01 06:51:33 +00:00
|
|
|
'id': self.id,
|
2020-10-17 13:15:35 +00:00
|
|
|
'pair': self.pair,
|
2020-10-17 18:32:23 +00:00
|
|
|
'lock_time': self.lock_time.strftime(DATETIME_PRINT_FORMAT),
|
2020-10-17 15:58:07 +00:00
|
|
|
'lock_timestamp': int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000),
|
2020-10-17 18:32:23 +00:00
|
|
|
'lock_end_time': self.lock_end_time.strftime(DATETIME_PRINT_FORMAT),
|
2020-10-17 15:58:07 +00:00
|
|
|
'lock_end_timestamp': int(self.lock_end_time.replace(tzinfo=timezone.utc
|
|
|
|
).timestamp() * 1000),
|
2020-10-17 13:15:35 +00:00
|
|
|
'reason': self.reason,
|
|
|
|
'active': self.active,
|
|
|
|
}
|