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
2020-08-22 07:12:09 +00:00
from datetime import datetime , 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
2020-09-28 17:39:41 +00:00
from sqlalchemy import ( Boolean , Column , DateTime , Float , ForeignKey , Integer , String ,
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
2020-10-17 09:25:42 +00:00
from freqtrade . constants import DATETIME_PRINT_FORMAT
2021-06-08 19:21:29 +00:00
from freqtrade . enums import SellType
2020-09-06 12:33:45 +00:00
from freqtrade . exceptions import DependencyException , OperationalException
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-06-22 03:26:31 +00:00
leverage = Column ( Float , nullable = True , default = 1.0 )
2021-06-27 02:36:19 +00:00
is_short = Column ( Boolean , nullable = True , default = False )
2020-08-13 12:13:58 +00:00
def __repr__ ( self ) :
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 )
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
2020-08-13 15:17:52 +00:00
if self . status in ( ' closed ' , ' canceled ' , ' cancelled ' ) :
self . ft_is_open = False
2020-08-22 07:12:09 +00:00
if order . get ( ' filled ' , 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
2021-06-20 04:06:51 +00:00
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-02-20 19:07:00 +00:00
stake_amount : float = 0.0
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 = ' '
timeframe : Optional [ int ] = None
2017-05-12 17:11:56 +00:00
2021-06-20 08:25:22 +00:00
# Margin trading properties
2021-06-20 09:01:03 +00:00
borrowed_currency : str = None
collateral_currency : str = None
2021-06-20 08:25:22 +00:00
interest_rate : float = 0.0
2021-06-20 09:01:03 +00:00
liquidation_price : float = None
2021-06-27 02:36:19 +00:00
__leverage : float = 1.0 # * You probably want to use self.leverage instead |
__borrowed : float = 0.0 # * You probably want to use self.borrowed instead |
__is_short : bool = False # * You probably want to use self.is_short instead V
@property
def leverage ( self ) - > float :
return self . __leverage or 1.0
@property
def borrowed ( self ) - > float :
return self . __borrowed or 0.0
@property
def is_short ( self ) - > bool :
return self . __is_short or False
@is_short.setter
2021-06-27 03:34:58 +00:00
def is_short ( self , val : bool ) :
2021-06-27 02:36:19 +00:00
self . __is_short = val
@leverage.setter
2021-06-27 03:34:58 +00:00
def leverage ( self , lev : float ) :
2021-06-27 02:36:19 +00:00
self . __leverage = lev
self . __borrowed = self . amount * ( lev - 1 )
self . amount = self . amount * lev
@borrowed.setter
2021-06-27 03:34:58 +00:00
def borrowed ( self , bor : float ) :
2021-06-27 02:36:19 +00:00
self . __leverage = self . amount / ( self . amount - self . borrowed )
self . __borrowed = bor
2021-06-20 08:25:22 +00:00
# End of margin trading properties
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 )
2019-12-17 06:02:02 +00:00
def __init__ ( self , * * kwargs ) :
2021-06-27 02:36:19 +00:00
if kwargs . get ( ' leverage ' ) and kwargs . get ( ' borrowed ' ) :
# TODO-mg: should I raise an error?
2021-06-20 08:25:22 +00:00
raise OperationalException ( ' Cannot pass both borrowed and leverage to Trade ' )
2021-02-20 18:29:04 +00:00
for key in kwargs :
setattr ( self , key , kwargs [ key ] )
2020-12-10 18:21:20 +00:00
self . recalc_open_trade_value ( )
2019-12-17 06:02:02 +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 '
2018-06-23 13:27:29 +00:00
return ( f ' Trade(id= { self . id } , pair= { self . pair } , amount= { self . amount : .8f } , '
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 ,
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 ,
' borrowed ' : self . borrowed ,
' borrowed_currency ' : self . borrowed_currency ,
2021-06-20 09:01:03 +00:00
' collateral_currency ' : self . collateral_currency ,
2021-06-20 04:06:51 +00:00
' interest_rate ' : self . interest_rate ,
2021-06-20 09:01:03 +00:00
' liquidation_price ' : self . liquidation_price ,
2021-06-22 03:26:31 +00:00
' is_short ' : self . is_short ,
2021-06-20 04:06:51 +00:00
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
2020-02-02 04:00:40 +00:00
def adjust_min_max_rates ( self , current_price : 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 )
self . min_rate = min ( current_price , self . min_rate or self . open_rate )
2019-03-16 18:54:16 +00:00
2020-12-11 06:41:06 +00:00
def _set_new_stoploss ( self , new_loss : float , stoploss : float ) :
""" Assign new stop value """
self . stop_loss = new_loss
self . stop_loss_pct = - 1 * abs ( stoploss )
self . stoploss_last_update = datetime . utcnow ( )
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
2018-06-26 20:41:28 +00:00
new_loss = float ( current_price * ( 1 - abs ( stoploss ) ) )
2021-06-20 08:25:22 +00:00
# TODO: Could maybe move this if into the new stoploss if branch
2021-06-20 09:01:03 +00:00
if ( self . liquidation_price ) : # If trading on margin, don't set the stoploss below the liquidation price
new_loss = min ( self . liquidation_price , 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... " )
2020-12-11 06:41:06 +00:00
self . _set_new_stoploss ( new_loss , stoploss )
2018-06-26 18:49:07 +00:00
self . initial_stop_loss = new_loss
2019-03-31 11:15:35 +00:00
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-06-20 08:25:22 +00:00
# stop losses only walk up, never down!, #TODO: But adding more to a margin account would create a lower liquidation price, decreasing the minimum stoploss
if ( new_loss > self . stop_loss and not self . is_short ) or ( new_loss < self . stop_loss and self . is_short ) :
2019-09-11 20:32:08 +00:00
logger . debug ( f " { self . pair } - Adjusting stoploss... " )
2020-12-11 06:41:06 +00:00
self . _set_new_stoploss ( 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
2021-06-20 04:06:51 +00:00
def is_opening_trade ( self , side ) - > bool :
"""
Determines if the trade is an opening ( long buy or short sell ) trade
: param side ( string ) : the side ( buy / sell ) that order happens on
"""
2021-06-27 02:36:19 +00:00
is_short = self . is_short or False
2021-06-23 04:26:10 +00:00
return ( side == ' buy ' and not is_short ) or ( side == ' sell ' and is_short )
2021-06-20 08:25:22 +00:00
2021-06-20 04:06:51 +00:00
def is_closing_trade ( self , side ) - > bool :
"""
Determines if the trade is an closing ( long sell or short buy ) trade
: param side ( string ) : the side ( buy / sell ) that order happens on
"""
2021-06-27 02:36:19 +00:00
is_short = self . is_short or False
2021-06-23 04:26:10 +00:00
return ( side == ' sell ' and not is_short ) or ( side == ' buy ' and is_short )
2021-06-20 04:06:51 +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
# if ('leverage' in order and 'borrowed' in order):
# raise OperationalException('Cannot update a trade with both borrowed and leverage')
2021-06-23 04:26:10 +00:00
# TODO: I don't like this, but it might be the only way
if ' is_short ' in order and order [ ' side ' ] == ' sell ' :
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-06-20 09:01:03 +00:00
if order_type in ( ' market ' , ' limit ' ) and self . is_opening_trade ( order [ ' side ' ] ) :
2017-10-31 23:54:16 +00:00
# Update open rate and actual amount
2021-06-27 02:36:19 +00:00
# self.is_short = safe_value_fallback(order, 'is_short', default_value=False)
# self.borrowed = safe_value_fallback(order, 'is_short', default_value=False)
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-06-27 02:36:19 +00:00
if ' borrowed ' in order :
self . borrowed = order [ ' borrowed ' ]
elif ' leverage ' in order :
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-06-20 09:01:03 +00:00
elif order_type in ( ' market ' , ' limit ' ) and self . is_closing_trade ( 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-06-22 03:26:31 +00:00
logger . info ( f ' { order_type . upper ( ) } _ { payment } has been fulfilled for { self } . ' )
2021-06-20 08:25:22 +00:00
self . close ( safe_value_fallback ( order , ' average ' , ' price ' ) ) # TODO: Double check this
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-06-20 04:06:51 +00:00
if self . is_opening_trade ( 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-06-20 04:06:51 +00:00
elif self . is_closing_trade ( 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-06-20 04:06:51 +00:00
if self . is_opening_trade ( side ) :
2020-05-01 17:54:16 +00:00
return self . fee_open_currency is not None
2021-06-20 04:06:51 +00:00
elif self . is_closing_trade ( 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 )
2020-12-10 18:21:20 +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-06-20 04:06:51 +00:00
open_trade = Decimal ( self . amount ) * Decimal ( self . open_rate )
fees = open_trade * Decimal ( self . fee_open )
2021-06-20 08:25:22 +00:00
if ( self . is_short ) :
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 .
2019-12-17 06:08:36 +00:00
Must be called whenever open_rate or fee_open is changed .
"""
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-23 03:09:52 +00:00
def calculate_interest ( self ) - > Decimal :
# TODO-mg: Need to set other conditions because sometimes self.open_date is not defined, but why would it ever not be set
if not self . interest_rate or not ( self . borrowed ) :
return Decimal ( 0.0 )
try :
open_date = self . open_date . replace ( tzinfo = None )
now = datetime . now ( )
secPerDay = 86400
days = Decimal ( ( now - open_date ) . total_seconds ( ) / secPerDay ) or 0.0
hours = days / 24
except :
raise OperationalException ( " Time isn ' t calculated properly " )
rate = Decimal ( self . interest_rate )
borrowed = Decimal ( self . borrowed )
2021-06-23 04:26:10 +00:00
twenty4 = Decimal ( 24.0 )
one = Decimal ( 1.0 )
2021-06-23 03:09:52 +00:00
if self . exchange == ' binance ' :
# Rate is per day but accrued hourly or something
# binance: https://www.binance.com/en-AU/support/faq/360030157812
2021-06-23 04:26:10 +00:00
return borrowed * ( rate / twenty4 ) * max ( hours , one ) # TODO-mg: Is hours rounded?
2021-06-23 03:09:52 +00:00
elif self . exchange == ' kraken ' :
# https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading-
opening_fee = borrowed * rate
roll_over_fee = borrowed * rate * max ( 0 , ( hours - 4 ) / 4 )
return opening_fee + roll_over_fee
elif self . exchange == ' binance_usdm_futures ' :
# ! TODO-mg: This is incorrect, I didn't look it up
2021-06-23 04:26:10 +00:00
return borrowed * ( rate / twenty4 ) * max ( hours , one )
2021-06-23 03:09:52 +00:00
elif self . exchange == ' binance_coinm_futures ' :
# ! TODO-mg: This is incorrect, I didn't look it up
2021-06-23 04:26:10 +00:00
return borrowed * ( rate / twenty4 ) * max ( hours , one )
2021-06-23 03:09:52 +00:00
else :
# TODO-mg: make sure this breaks and can't be squelched
raise OperationalException ( " Leverage not available on this exchange " )
2020-12-10 18:21:20 +00:00
def calc_close_trade_value ( self , rate : Optional [ float ] = None ,
2019-09-10 07:42:45 +00:00
fee : 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
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-06-23 03:09:52 +00:00
interest = self . calculate_interest ( )
2021-06-23 04:26:10 +00:00
if self . is_short :
amount = Decimal ( self . amount ) + interest
else :
amount = Decimal ( self . amount ) - interest
close_trade = Decimal ( amount ) * Decimal ( rate or self . close_rate ) # type: ignore
fees = close_trade * Decimal ( fee or self . fee_close )
2021-06-22 03:26:31 +00:00
2021-06-20 08:25:22 +00:00
if ( self . is_short ) :
2021-06-23 04:26:10 +00:00
return float ( close_trade + fees )
2021-06-20 04:06:51 +00:00
else :
2021-06-23 04:26:10 +00:00
return float ( close_trade - fees )
2017-12-17 21:07:56 +00:00
2019-09-10 07:42:45 +00:00
def calc_profit ( self , rate : Optional [ float ] = None ,
fee : 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
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 ) ,
2018-04-21 17:47:08 +00:00
fee = ( fee or self . fee_close )
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-20 04:19:09 +00:00
fee : 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 ) .
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 ) ,
2018-04-21 17:47:08 +00:00
fee = ( fee or self . fee_close )
2017-12-17 21:07:56 +00:00
)
2021-06-20 08:25:22 +00:00
if self . is_short :
2021-06-22 03:26:31 +00:00
if close_trade_value == 0.0 :
return 0.0
else :
profit_ratio = ( self . open_trade_value / close_trade_value ) - 1
2021-06-20 04:06:51 +00:00
else :
2021-06-22 03:26:31 +00:00
if self . open_trade_value == 0.0 :
return 0.0
else :
profit_ratio = ( close_trade_value / self . open_trade_value ) - 1
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-23 03:09:52 +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-23 03:09:52 +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-23 03:09:52 +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-23 03:09:52 +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-23 03:09:52 +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
and trade . initial_stop_loss_pct != desired_stoploss ) :
# 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 )
amount = Column ( Float )
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-06-22 03:26:31 +00:00
sell_reason = Column ( String ( 100 ) , nullable = True ) # TODO: Change to close_reason
sell_order_status = Column ( String ( 100 ) , nullable = True )
2021-05-22 08:12:23 +00:00
strategy = Column ( String ( 100 ) , nullable = True )
2021-02-20 18:29:04 +00:00
timeframe = Column ( Integer , nullable = True )
2021-06-20 08:25:22 +00:00
# Margin trading properties
2021-06-22 03:26:31 +00:00
leverage = Column ( Float , nullable = True , default = 1.0 )
2021-06-20 04:19:09 +00:00
borrowed = Column ( Float , nullable = False , default = 0.0 )
borrowed_currency = Column ( Float , nullable = True )
2021-06-20 09:01:03 +00:00
collateral_currency = Column ( String ( 25 ) , nullable = True )
2021-06-20 04:19:09 +00:00
interest_rate = Column ( Float , nullable = False , default = 0.0 )
2021-06-20 09:01:03 +00:00
liquidation_price = Column ( Float , nullable = True )
2021-06-20 08:25:22 +00:00
is_short = Column ( Boolean , nullable = False , default = False )
# End of margin trading properties
2021-06-20 04:19:09 +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-23 03:09:52 +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-23 03:09:52 +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-23 03:09:52 +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-23 03:09:52 +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-23 03:09:52 +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-23 03:09:52 +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-23 03:09:52 +00:00
@ staticmethod
2021-04-27 18:25:36 +00:00
def get_overall_performance ( ) - > List [ Dict [ str , Any ] ] :
"""
Returns List of dicts containing all Trades , including profit and trade count
NOTE : Not supported in Backtesting .
"""
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 ' )
) . filter ( Trade . is_open . is_ ( False ) ) \
. 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-05-15 17:39:46 +00:00
' profit ' : profit ,
' 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
]
@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 } , '
f ' lock_end_time= { lock_end_time } ) ' )
2021-06-23 03:09:52 +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 ,
}