From a27171b3710c7d59e30f16b7d58615a31a9cc74c Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 19 Jun 2021 22:06:51 -0600 Subject: [PATCH 01/46] Updated LocalTrade and Order classes --- freqtrade/persistence/migrations.py | 10 +-- freqtrade/persistence/models.py | 134 ++++++++++++++++++++-------- 2 files changed, 101 insertions(+), 43 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 00c9b91eb..12e182326 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -45,7 +45,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') max_rate = get_column_def(cols, 'max_rate', '0.0') min_rate = get_column_def(cols, 'min_rate', 'null') - sell_reason = get_column_def(cols, 'sell_reason', 'null') + close_reason = get_column_def(cols, 'close_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') # If ticker-interval existed use that, else null. if has_column(cols, 'ticker_interval'): @@ -58,7 +58,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col close_profit_abs = get_column_def( cols, 'close_profit_abs', f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}") - sell_order_status = get_column_def(cols, 'sell_order_status', 'null') + close_order_status = get_column_def(cols, 'close_order_status', 'null') amount_requested = get_column_def(cols, 'amount_requested', 'amount') # Schema migration necessary @@ -80,7 +80,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stake_amount, amount, amount_requested, open_date, close_date, open_order_id, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stoploss_order_id, stoploss_last_update, - max_rate, min_rate, sell_reason, sell_order_status, strategy, + max_rate, min_rate, close_reason, close_order_status, strategy, timeframe, open_trade_value, close_profit_abs ) select id, lower(exchange), @@ -101,8 +101,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {initial_stop_loss} initial_stop_loss, {initial_stop_loss_pct} initial_stop_loss_pct, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, - {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, - {sell_order_status} sell_order_status, + {max_rate} max_rate, {min_rate} min_rate, {close_reason} close_reason, + {close_order_status} close_order_status, {strategy} strategy, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs from {table_back_name} diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8dcfc6c94..49d8f9eaf 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -29,13 +29,13 @@ _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database def init_db(db_url: str, clean_open_orders: bool = False) -> None: """ - 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 + 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 """ kwargs = {} @@ -131,9 +131,10 @@ class Order(_DECL_BASE): order_date = Column(DateTime, nullable=True, default=datetime.utcnow) order_filled_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True) + + leverage = Column(Float, nullable=True) def __repr__(self): - return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' f'side={self.side}, order_type={self.order_type}, status={self.status})') @@ -155,6 +156,7 @@ class Order(_DECL_BASE): self.average = order.get('average', self.average) self.remaining = order.get('remaining', self.remaining) self.cost = order.get('cost', self.cost) + self.leverage = order.get('leverage', self.leverage) if 'timestamp' in order and order['timestamp'] is not None: self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc) @@ -226,6 +228,7 @@ class LocalTrade(): fee_close_currency: str = '' open_rate: float = 0.0 open_rate_requested: Optional[float] = None + # open_trade_value - calculated via _calc_open_trade_value open_trade_value: float = 0.0 close_rate: Optional[float] = None @@ -254,11 +257,20 @@ class LocalTrade(): max_rate: float = 0.0 # Lowest price reached min_rate: float = 0.0 - sell_reason: str = '' - sell_order_status: str = '' + close_reason: str = '' + close_order_status: str = '' strategy: str = '' timeframe: Optional[int] = None + #Margin trading properties + leverage: float = 1.0 + borrowed: float = 0 + borrowed_currency: float = None + interest_rate: float = 0 + min_stoploss: float = None + isShort: boolean = False + #End of margin trading properties + def __init__(self, **kwargs): for key in kwargs: setattr(self, key, kwargs[key]) @@ -322,8 +334,8 @@ class LocalTrade(): 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'profit_abs': self.close_profit_abs, - 'sell_reason': self.sell_reason, - 'sell_order_status': self.sell_order_status, + 'close_reason': self.close_reason, + 'close_order_status': self.close_order_status, 'stop_loss_abs': self.stop_loss, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, @@ -340,6 +352,13 @@ class LocalTrade(): 'min_rate': self.min_rate, 'max_rate': self.max_rate, + 'leverage': self.leverage, + 'borrowed': self.borrowed, + 'borrowed_currency': self.borrowed_currency, + 'interest_rate': self.interest_rate, + 'min_stoploss': self.min_stoploss, + 'leverage': self.leverage, + 'open_order_id': self.open_order_id, } @@ -379,6 +398,9 @@ class LocalTrade(): return new_loss = float(current_price * (1 - abs(stoploss))) + #TODO: Could maybe move this if into the new stoploss if branch + if (self.min_stoploss): #If trading on margin, don't set the stoploss below the liquidation price + new_loss = min(self.min_stoploss, new_loss) # no stop loss assigned yet if not self.stop_loss: @@ -389,7 +411,7 @@ class LocalTrade(): # evaluate if the stop loss needs to be updated else: - if new_loss > self.stop_loss: # stop losses only walk up, never down! + if (new_loss > self.stop_loss and not self.isShort) or (new_loss < self.stop_loss and self.isShort): # 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 logger.debug(f"{self.pair} - Adjusting stoploss...") self._set_new_stoploss(new_loss, stoploss) else: @@ -403,6 +425,20 @@ class LocalTrade(): f"Trailing stoploss saved us: " f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.") + 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 + """ + return (side == 'buy' and not self.isShort) or (side == 'sell' and self.isShort) + + 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 + """ + return (side == 'sell' and not self.isShort) or (side == 'buy' and self.isShort) + def update(self, order: Dict) -> None: """ Updates this entity with amount and actual open/close rates. @@ -416,22 +452,24 @@ class LocalTrade(): logger.info('Updating trade (id=%s) ...', self.id) - if order_type in ('market', 'limit') and order['side'] == 'buy': + if order_type in ('market', 'limit') and self.isOpeningTrade(order['side']): # Update open rate and actual amount self.open_rate = float(safe_value_fallback(order, 'average', 'price')) self.amount = float(safe_value_fallback(order, 'filled', 'amount')) self.recalc_open_trade_value() if self.is_open: - logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') + payment = "SELL" if self.isShort else "BUY" + logger.info(f'{order_type.upper()}_{payment} order has been fulfilled for {self}.') self.open_order_id = None - elif order_type in ('market', 'limit') and order['side'] == 'sell': + elif order_type in ('market', 'limit') and self.isClosingTrade(order['side']): if self.is_open: - logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') - self.close(safe_value_fallback(order, 'average', 'price')) + payment = "BUY" if self.isShort else "SELL" + logger.info(f'{order_type.upper()}_{payment} order has been fulfilled for {self}.') + self.close(safe_value_fallback(order, 'average', 'price')) #TODO: Double check this elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): self.stoploss_order_id = None self.close_rate_requested = self.stop_loss - self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value + self.close_reason = SellType.STOPLOSS_ON_EXCHANGE.value if self.is_open: logger.info(f'{order_type.upper()} is hit for {self}.') self.close(safe_value_fallback(order, 'average', 'price')) @@ -445,11 +483,11 @@ class LocalTrade(): and marks trade as closed """ self.close_rate = rate + self.close_date = self.close_date or datetime.utcnow() self.close_profit = self.calc_profit_ratio() self.close_profit_abs = self.calc_profit() - self.close_date = self.close_date or datetime.utcnow() self.is_open = False - self.sell_order_status = 'closed' + self.close_order_status = 'closed' self.open_order_id = None if show_msg: logger.info( @@ -462,14 +500,14 @@ class LocalTrade(): """ Update Fee parameters. Only acts once per side """ - if side == 'buy' and self.fee_open_currency is None: + if self.is_opening_trade(side) and self.fee_open_currency is None: 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 - elif side == 'sell' and self.fee_close_currency is None: + elif self.is_closing_trade(side) and self.fee_close_currency is None: self.fee_close_cost = fee_cost self.fee_close_currency = fee_currency if fee_rate is not None: @@ -479,9 +517,9 @@ class LocalTrade(): """ Verify if this side (buy / sell) has already been updated """ - if side == 'buy': + if self.is_opening_trade(side): return self.fee_open_currency is not None - elif side == 'sell': + elif self.is_closing_trade(side): return self.fee_close_currency is not None else: return False @@ -494,9 +532,13 @@ class LocalTrade(): Calculate the open_rate including open_fee. :return: Price in of the open trade incl. Fees """ - buy_trade = Decimal(self.amount) * Decimal(self.open_rate) - fees = buy_trade * Decimal(self.fee_open) - return float(buy_trade + fees) + open_trade = Decimal(self.amount) * Decimal(self.open_rate) + fees = open_trade * Decimal(self.fee_open) + if (self.isShort): + return float(open_trade - fees) + else: + return float(open_trade + fees) + def recalc_open_trade_value(self) -> None: """ @@ -513,34 +555,47 @@ class LocalTrade(): If rate is not set self.fee will be used :param rate: rate to compare with (optional). If rate is not set self.close_rate will be used + :param borrowed: amount borrowed to make this trade + If borrowed is not set self.borrowed will be used :return: Price in BTC of the open trade """ if rate is None and not self.close_rate: return 0.0 - sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore - fees = sell_trade * Decimal(fee or self.fee_close) - return float(sell_trade - fees) + close_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore + fees = close_trade * Decimal(fee or self.fee_close) + if (self.isShort): + interest = (self.interest_rate * Decimal(borrowed or self.borrowed)) * (datetime.utcnow() - self.open_date).days #Interest/day * num of days, 0 if not margin + return float(close_trade + fees + interest) + else: + return float(close_trade - fees) def calc_profit(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float: """ Calculate the absolute profit in stake currency between Close and Open trade :param fee: fee to use on the close rate (optional). - If rate is not set self.fee will be used + If fee is not set self.fee will be used :param rate: close rate to compare with (optional). If rate is not set self.close_rate will be used + :param borrowed: amount borrowed to make this trade + If borrowed is not set self.borrowed will be used :return: profit in stake currency as float """ close_trade_value = self.calc_close_trade_value( rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) - profit = close_trade_value - self.open_trade_value + + if self.isShort: + profit = self.open_trade_value - close_trade_value + else: + profit = close_trade_value - self.open_trade_value return float(f"{profit:.8f}") def calc_profit_ratio(self, rate: Optional[float] = None, - fee: Optional[float] = None) -> float: + fee: Optional[float] = None, + borrowed: Optional[float] = None) -> float: """ Calculates the profit as ratio (including fee). :param rate: rate to compare with (optional). @@ -554,7 +609,10 @@ class LocalTrade(): ) if self.open_trade_value == 0.0: return 0.0 - profit_ratio = (close_trade_value / self.open_trade_value) - 1 + if self.isShort: + profit_ratio = (close_trade_value / self.open_trade_value) - 1 + else: + profit_ratio = (self.open_trade_value / close_trade_value) - 1 return float(f"{profit_ratio:.8f}") def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: @@ -604,7 +662,7 @@ class LocalTrade(): sel_trades = [trade for trade in sel_trades if trade.close_date and trade.close_date > close_date] - return sel_trades + return sel_trades #TODO: What is sel_trades does it mean sell_trades? If so, update this for margin @staticmethod def close_bt_trade(trade): @@ -700,8 +758,8 @@ class Trade(_DECL_BASE, LocalTrade): max_rate = Column(Float, nullable=True, default=0.0) # Lowest price reached min_rate = Column(Float, nullable=True) - sell_reason = Column(String(100), nullable=True) - sell_order_status = Column(String(100), nullable=True) + close_reason = Column(String(100), nullable=True) + close_order_status = Column(String(100), nullable=True) strategy = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) From 69e81100e458d92599c05685fb400c562dc98795 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 19 Jun 2021 22:19:09 -0600 Subject: [PATCH 02/46] Updated Trade class --- freqtrade/freqtradebot.py | 2 +- freqtrade/persistence/models.py | 34 +++++++++++++++++++++++---------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d430dbc48..5d12c4cd4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -268,7 +268,7 @@ class FreqtradeBot(LoggingMixin): # Updating open orders in dry-run does not make sense and will fail. return - trades: List[Trade] = Trade.get_sold_trades_without_assigned_fees() + trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees() for trade in trades: if not trade.is_open and not trade.fee_updated('sell'): diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 49d8f9eaf..e95d3a9f5 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -263,7 +263,7 @@ class LocalTrade(): timeframe: Optional[int] = None #Margin trading properties - leverage: float = 1.0 + leverage: Optional[float] = None borrowed: float = 0 borrowed_currency: float = None interest_rate: float = 0 @@ -555,8 +555,6 @@ class LocalTrade(): If rate is not set self.fee will be used :param rate: rate to compare with (optional). If rate is not set self.close_rate will be used - :param borrowed: amount borrowed to make this trade - If borrowed is not set self.borrowed will be used :return: Price in BTC of the open trade """ if rate is None and not self.close_rate: @@ -564,11 +562,11 @@ class LocalTrade(): close_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore fees = close_trade * Decimal(fee or self.fee_close) + interest = ((self.interest_rate * Decimal(borrowed or self.borrowed)) * (datetime.utcnow() - self.open_date).days) or 0 #Interest/day * num of days if (self.isShort): - interest = (self.interest_rate * Decimal(borrowed or self.borrowed)) * (datetime.utcnow() - self.open_date).days #Interest/day * num of days, 0 if not margin return float(close_trade + fees + interest) else: - return float(close_trade - fees) + return float(close_trade - fees - interest) def calc_profit(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float: @@ -578,8 +576,6 @@ class LocalTrade(): If fee is not set self.fee will be used :param rate: close rate to compare with (optional). If rate is not set self.close_rate will be used - :param borrowed: amount borrowed to make this trade - If borrowed is not set self.borrowed will be used :return: profit in stake currency as float """ close_trade_value = self.calc_close_trade_value( @@ -594,8 +590,7 @@ class LocalTrade(): return float(f"{profit:.8f}") def calc_profit_ratio(self, rate: Optional[float] = None, - fee: Optional[float] = None, - borrowed: Optional[float] = None) -> float: + fee: Optional[float] = None) -> float: """ Calculates the profit as ratio (including fee). :param rate: rate to compare with (optional). @@ -763,7 +758,26 @@ class Trade(_DECL_BASE, LocalTrade): strategy = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) + #Margin trading properties + leverage = Column(Float, nullable=True) + borrowed = Column(Float, nullable=False, default=0.0) + borrowed_currency = Column(Float, nullable=True) + interest_rate = Column(Float, nullable=False, default=0.0) + min_stoploss = Column(Float, nullable=True) + isShort = Column(Boolean, nullable=False, default=False) + #End of margin trading properties + def __init__(self, **kwargs): + lev = kwargs.get('leverage') + bor = kwargs.get('borrowed') + amount = kwargs.get('amount') + if lev and bor: + raise OperationalException('Cannot pass both borrowed and leverage to Trade') #TODO: should I raise an error? + elif lev: + self.amount = amount * lev + self.borrowed = amount * (lev-1) + elif bor: + self.lev = (bor + amount)/amount super().__init__(**kwargs) self.recalc_open_trade_value() @@ -849,7 +863,7 @@ class Trade(_DECL_BASE, LocalTrade): ]).all() @staticmethod - def get_sold_trades_without_assigned_fees(): + def get_closed_trades_without_assigned_fees(): """ Returns all closed trades which don't have fees set correctly NOTE: Not supported in Backtesting. From 20dcd9a1a21d06013fa49bbc21c5f6847f40c3e2 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 20 Jun 2021 02:25:22 -0600 Subject: [PATCH 03/46] Added changed to persistance/migrations --- freqtrade/persistence/migrations.py | 20 +++++-- freqtrade/persistence/models.py | 87 +++++++++++++++-------------- 2 files changed, 60 insertions(+), 47 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 12e182326..5922f6a0e 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -47,6 +47,13 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col min_rate = get_column_def(cols, 'min_rate', 'null') close_reason = get_column_def(cols, 'close_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') + + leverage = get_column_def(cols, 'leverage', '0.0') + borrowed = get_column_def(cols, 'borrowed', '0.0') + borrowed_currency = get_column_def(cols, 'borrowed_currency', 'null') + interest_rate = get_column_def(cols, 'interest_rate', '0.0') + min_stoploss = get_column_def(cols, 'min_stoploss', 'null') + is_short = get_column_def(cols, 'is_short', 'False') # If ticker-interval existed use that, else null. if has_column(cols, 'ticker_interval'): timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') @@ -81,7 +88,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stoploss_order_id, stoploss_last_update, max_rate, min_rate, close_reason, close_order_status, strategy, - timeframe, open_trade_value, close_profit_abs + timeframe, open_trade_value, close_profit_abs, + leverage, borrowed, borrowed_currency, interest_rate, min_stoploss, is_short ) select id, lower(exchange), case @@ -104,11 +112,13 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {max_rate} max_rate, {min_rate} min_rate, {close_reason} close_reason, {close_order_status} close_order_status, {strategy} strategy, {timeframe} timeframe, - {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs + {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, + {leverage} leverage, {borrowed} borrowed, {borrowed_currency} borrowed_currency, + {interest_rate} interest_rate, {min_stoploss} min_stoploss, {is_short} is_short from {table_back_name} """)) - +#TODO: Does leverage go in here? def migrate_open_orders_to_trades(engine): with engine.begin() as connection: connection.execute(text(""" @@ -141,10 +151,10 @@ def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, col connection.execute(text(f""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, average, remaining, cost, - order_date, order_filled_date, order_update_date) + order_date, order_filled_date, order_update_date, leverage) select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, null average, remaining, cost, - order_date, order_filled_date, order_update_date + order_date, order_filled_date, order_update_date, leverage from {table_back_name} """)) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index e95d3a9f5..a7548d2b4 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -131,8 +131,8 @@ class Order(_DECL_BASE): order_date = Column(DateTime, nullable=True, default=datetime.utcnow) order_filled_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True) - - leverage = Column(Float, nullable=True) + + leverage = Column(Float, nullable=True, default=0.0) def __repr__(self): return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' @@ -257,21 +257,33 @@ class LocalTrade(): max_rate: float = 0.0 # Lowest price reached min_rate: float = 0.0 - close_reason: str = '' - close_order_status: str = '' + close_reason: str = '' + close_order_status: str = '' strategy: str = '' timeframe: Optional[int] = None - #Margin trading properties - leverage: Optional[float] = None - borrowed: float = 0 + # Margin trading properties + leverage: Optional[float] = 0.0 + borrowed: float = 0.0 borrowed_currency: float = None - interest_rate: float = 0 + interest_rate: float = 0.0 min_stoploss: float = None - isShort: boolean = False - #End of margin trading properties + is_short: bool = False + # End of margin trading properties def __init__(self, **kwargs): + lev = kwargs.get('leverage') + bor = kwargs.get('borrowed') + amount = kwargs.get('amount') + if lev and bor: + # TODO: should I raise an error? + raise OperationalException('Cannot pass both borrowed and leverage to Trade') + elif lev: + self.amount = amount * lev + self.borrowed = amount * (lev-1) + elif bor: + self.lev = (bor + amount)/amount + for key in kwargs: setattr(self, key, kwargs[key]) self.recalc_open_trade_value() @@ -398,8 +410,8 @@ class LocalTrade(): return new_loss = float(current_price * (1 - abs(stoploss))) - #TODO: Could maybe move this if into the new stoploss if branch - if (self.min_stoploss): #If trading on margin, don't set the stoploss below the liquidation price + # TODO: Could maybe move this if into the new stoploss if branch + if (self.min_stoploss): # If trading on margin, don't set the stoploss below the liquidation price new_loss = min(self.min_stoploss, new_loss) # no stop loss assigned yet @@ -411,7 +423,8 @@ class LocalTrade(): # evaluate if the stop loss needs to be updated else: - if (new_loss > self.stop_loss and not self.isShort) or (new_loss < self.stop_loss and self.isShort): # 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 + # 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): logger.debug(f"{self.pair} - Adjusting stoploss...") self._set_new_stoploss(new_loss, stoploss) else: @@ -430,14 +443,14 @@ class LocalTrade(): Determines if the trade is an opening (long buy or short sell) trade :param side (string): the side (buy/sell) that order happens on """ - return (side == 'buy' and not self.isShort) or (side == 'sell' and self.isShort) - + return (side == 'buy' and not self.is_short) or (side == 'sell' and self.is_short) + 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 """ - return (side == 'sell' and not self.isShort) or (side == 'buy' and self.isShort) + return (side == 'sell' and not self.is_short) or (side == 'buy' and self.is_short) def update(self, order: Dict) -> None: """ @@ -458,14 +471,14 @@ class LocalTrade(): self.amount = float(safe_value_fallback(order, 'filled', 'amount')) self.recalc_open_trade_value() if self.is_open: - payment = "SELL" if self.isShort else "BUY" + payment = "SELL" if self.is_short else "BUY" logger.info(f'{order_type.upper()}_{payment} order has been fulfilled for {self}.') self.open_order_id = None elif order_type in ('market', 'limit') and self.isClosingTrade(order['side']): if self.is_open: - payment = "BUY" if self.isShort else "SELL" + payment = "BUY" if self.is_short else "SELL" logger.info(f'{order_type.upper()}_{payment} order has been fulfilled for {self}.') - self.close(safe_value_fallback(order, 'average', 'price')) #TODO: Double check this + self.close(safe_value_fallback(order, 'average', 'price')) # TODO: Double check this elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): self.stoploss_order_id = None self.close_rate_requested = self.stop_loss @@ -534,11 +547,10 @@ class LocalTrade(): """ open_trade = Decimal(self.amount) * Decimal(self.open_rate) fees = open_trade * Decimal(self.fee_open) - if (self.isShort): - return float(open_trade - fees) + if (self.is_short): + return float(open_trade - fees) else: - return float(open_trade + fees) - + return float(open_trade + fees) def recalc_open_trade_value(self) -> None: """ @@ -562,8 +574,9 @@ class LocalTrade(): close_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore fees = close_trade * Decimal(fee or self.fee_close) - interest = ((self.interest_rate * Decimal(borrowed or self.borrowed)) * (datetime.utcnow() - self.open_date).days) or 0 #Interest/day * num of days - if (self.isShort): + interest = ((self.interest_rate * Decimal(borrowed or self.borrowed)) * + (datetime.utcnow() - self.open_date).days) or 0 # Interest/day * num of days + if (self.is_short): return float(close_trade + fees + interest) else: return float(close_trade - fees - interest) @@ -583,7 +596,7 @@ class LocalTrade(): fee=(fee or self.fee_close) ) - if self.isShort: + if self.is_short: profit = self.open_trade_value - close_trade_value else: profit = close_trade_value - self.open_trade_value @@ -604,7 +617,7 @@ class LocalTrade(): ) if self.open_trade_value == 0.0: return 0.0 - if self.isShort: + if self.is_short: profit_ratio = (close_trade_value / self.open_trade_value) - 1 else: profit_ratio = (self.open_trade_value / close_trade_value) - 1 @@ -657,7 +670,7 @@ class LocalTrade(): sel_trades = [trade for trade in sel_trades if trade.close_date and trade.close_date > close_date] - return sel_trades #TODO: What is sel_trades does it mean sell_trades? If so, update this for margin + return sel_trades # TODO: What is sel_trades does it mean sell_trades? If so, update this for margin @staticmethod def close_bt_trade(trade): @@ -758,26 +771,16 @@ class Trade(_DECL_BASE, LocalTrade): strategy = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) - #Margin trading properties - leverage = Column(Float, nullable=True) + # Margin trading properties + leverage = Column(Float, nullable=True, default=0.0) borrowed = Column(Float, nullable=False, default=0.0) borrowed_currency = Column(Float, nullable=True) interest_rate = Column(Float, nullable=False, default=0.0) min_stoploss = Column(Float, nullable=True) - isShort = Column(Boolean, nullable=False, default=False) - #End of margin trading properties + is_short = Column(Boolean, nullable=False, default=False) + # End of margin trading properties def __init__(self, **kwargs): - lev = kwargs.get('leverage') - bor = kwargs.get('borrowed') - amount = kwargs.get('amount') - if lev and bor: - raise OperationalException('Cannot pass both borrowed and leverage to Trade') #TODO: should I raise an error? - elif lev: - self.amount = amount * lev - self.borrowed = amount * (lev-1) - elif bor: - self.lev = (bor + amount)/amount super().__init__(**kwargs) self.recalc_open_trade_value() From 67341aa4f28122226df5ea8d487bd18765efb0f9 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 20 Jun 2021 03:01:03 -0600 Subject: [PATCH 04/46] Added changes suggested in pull request, fixed breaking changes, can run the bot again --- freqtrade/persistence/migrations.py | 14 ++++++------ freqtrade/persistence/models.py | 33 ++++++++++++++++------------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 5922f6a0e..298b18775 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -45,14 +45,15 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') max_rate = get_column_def(cols, 'max_rate', '0.0') min_rate = get_column_def(cols, 'min_rate', 'null') - close_reason = get_column_def(cols, 'close_reason', 'null') + sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') leverage = get_column_def(cols, 'leverage', '0.0') borrowed = get_column_def(cols, 'borrowed', '0.0') borrowed_currency = get_column_def(cols, 'borrowed_currency', 'null') + collateral_currency = get_column_def(cols, 'collateral_currency', 'null') interest_rate = get_column_def(cols, 'interest_rate', '0.0') - min_stoploss = get_column_def(cols, 'min_stoploss', 'null') + liquidation_price = get_column_def(cols, 'liquidation_price', 'null') is_short = get_column_def(cols, 'is_short', 'False') # If ticker-interval existed use that, else null. if has_column(cols, 'ticker_interval'): @@ -87,9 +88,9 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stake_amount, amount, amount_requested, open_date, close_date, open_order_id, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stoploss_order_id, stoploss_last_update, - max_rate, min_rate, close_reason, close_order_status, strategy, + max_rate, min_rate, sell_reason, close_order_status, strategy, timeframe, open_trade_value, close_profit_abs, - leverage, borrowed, borrowed_currency, interest_rate, min_stoploss, is_short + leverage, borrowed, borrowed_currency, collateral_currency, interest_rate, liquidation_price, is_short ) select id, lower(exchange), case @@ -109,12 +110,13 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {initial_stop_loss} initial_stop_loss, {initial_stop_loss_pct} initial_stop_loss_pct, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, - {max_rate} max_rate, {min_rate} min_rate, {close_reason} close_reason, + {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {close_order_status} close_order_status, {strategy} strategy, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, {leverage} leverage, {borrowed} borrowed, {borrowed_currency} borrowed_currency, - {interest_rate} interest_rate, {min_stoploss} min_stoploss, {is_short} is_short + {collateral_currency} collateral_currency, {interest_rate} interest_rate, + {liquidation_price} liquidation_price, {is_short} is_short from {table_back_name} """)) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a7548d2b4..7af71ec89 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -257,7 +257,7 @@ class LocalTrade(): max_rate: float = 0.0 # Lowest price reached min_rate: float = 0.0 - close_reason: str = '' + sell_reason: str = '' close_order_status: str = '' strategy: str = '' timeframe: Optional[int] = None @@ -265,9 +265,10 @@ class LocalTrade(): # Margin trading properties leverage: Optional[float] = 0.0 borrowed: float = 0.0 - borrowed_currency: float = None + borrowed_currency: str = None + collateral_currency: str = None interest_rate: float = 0.0 - min_stoploss: float = None + liquidation_price: float = None is_short: bool = False # End of margin trading properties @@ -346,7 +347,7 @@ class LocalTrade(): 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'profit_abs': self.close_profit_abs, - 'close_reason': self.close_reason, + 'sell_reason': self.sell_reason, 'close_order_status': self.close_order_status, 'stop_loss_abs': self.stop_loss, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, @@ -367,8 +368,9 @@ class LocalTrade(): 'leverage': self.leverage, 'borrowed': self.borrowed, 'borrowed_currency': self.borrowed_currency, + 'collateral_currency': self.collateral_currency, 'interest_rate': self.interest_rate, - 'min_stoploss': self.min_stoploss, + 'liquidation_price': self.liquidation_price, 'leverage': self.leverage, 'open_order_id': self.open_order_id, @@ -411,8 +413,8 @@ class LocalTrade(): new_loss = float(current_price * (1 - abs(stoploss))) # TODO: Could maybe move this if into the new stoploss if branch - if (self.min_stoploss): # If trading on margin, don't set the stoploss below the liquidation price - new_loss = min(self.min_stoploss, new_loss) + 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) # no stop loss assigned yet if not self.stop_loss: @@ -465,7 +467,7 @@ class LocalTrade(): logger.info('Updating trade (id=%s) ...', self.id) - if order_type in ('market', 'limit') and self.isOpeningTrade(order['side']): + if order_type in ('market', 'limit') and self.is_opening_trade(order['side']): # Update open rate and actual amount self.open_rate = float(safe_value_fallback(order, 'average', 'price')) self.amount = float(safe_value_fallback(order, 'filled', 'amount')) @@ -474,7 +476,7 @@ class LocalTrade(): payment = "SELL" if self.is_short else "BUY" logger.info(f'{order_type.upper()}_{payment} order has been fulfilled for {self}.') self.open_order_id = None - elif order_type in ('market', 'limit') and self.isClosingTrade(order['side']): + elif order_type in ('market', 'limit') and self.is_closing_trade(order['side']): if self.is_open: payment = "BUY" if self.is_short else "SELL" logger.info(f'{order_type.upper()}_{payment} order has been fulfilled for {self}.') @@ -482,7 +484,7 @@ class LocalTrade(): elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): self.stoploss_order_id = None self.close_rate_requested = self.stop_loss - self.close_reason = SellType.STOPLOSS_ON_EXCHANGE.value + self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value if self.is_open: logger.info(f'{order_type.upper()} is hit for {self}.') self.close(safe_value_fallback(order, 'average', 'price')) @@ -574,8 +576,8 @@ class LocalTrade(): close_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore fees = close_trade * Decimal(fee or self.fee_close) - interest = ((self.interest_rate * Decimal(borrowed or self.borrowed)) * - (datetime.utcnow() - self.open_date).days) or 0 # Interest/day * num of days + #TODO: Interest rate could be hourly instead of daily + interest = ((Decimal(self.interest_rate) * Decimal(self.borrowed)) * Decimal((datetime.utcnow() - self.open_date).days)) or 0 # Interest/day * num of days if (self.is_short): return float(close_trade + fees + interest) else: @@ -670,7 +672,7 @@ class LocalTrade(): sel_trades = [trade for trade in sel_trades if trade.close_date and trade.close_date > close_date] - return sel_trades # TODO: What is sel_trades does it mean sell_trades? If so, update this for margin + return sel_trades @staticmethod def close_bt_trade(trade): @@ -766,7 +768,7 @@ class Trade(_DECL_BASE, LocalTrade): max_rate = Column(Float, nullable=True, default=0.0) # Lowest price reached min_rate = Column(Float, nullable=True) - close_reason = Column(String(100), nullable=True) + sell_reason = Column(String(100), nullable=True) #TODO: Change to close_reason close_order_status = Column(String(100), nullable=True) strategy = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) @@ -775,8 +777,9 @@ class Trade(_DECL_BASE, LocalTrade): leverage = Column(Float, nullable=True, default=0.0) borrowed = Column(Float, nullable=False, default=0.0) borrowed_currency = Column(Float, nullable=True) + collateral_currency = Column(String(25), nullable=True) interest_rate = Column(Float, nullable=False, default=0.0) - min_stoploss = Column(Float, nullable=True) + liquidation_price = Column(Float, nullable=True) is_short = Column(Boolean, nullable=False, default=False) # End of margin trading properties From 613eecf16a0345b188c5974e3cae8707d0268c2a Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 21 Jun 2021 21:26:31 -0600 Subject: [PATCH 05/46] Adding templates for leverage/short tests All previous pytests pass --- freqtrade/persistence/migrations.py | 13 +-- freqtrade/persistence/models.py | 53 +++++++----- tests/conftest.py | 121 +++++++++++++++++++++++++--- tests/conftest_trades.py | 4 +- tests/rpc/test_rpc.py | 17 ++++ tests/test_persistence.py | 28 ++++++- 6 files changed, 197 insertions(+), 39 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 298b18775..c4e6368c5 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -47,7 +47,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col min_rate = get_column_def(cols, 'min_rate', 'null') sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') - + leverage = get_column_def(cols, 'leverage', '0.0') borrowed = get_column_def(cols, 'borrowed', '0.0') borrowed_currency = get_column_def(cols, 'borrowed_currency', 'null') @@ -66,7 +66,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col close_profit_abs = get_column_def( cols, 'close_profit_abs', f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}") - close_order_status = get_column_def(cols, 'close_order_status', 'null') + # TODO-mg: update to exit order status + sell_order_status = get_column_def(cols, 'sell_order_status', 'null') amount_requested = get_column_def(cols, 'amount_requested', 'amount') # Schema migration necessary @@ -88,7 +89,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stake_amount, amount, amount_requested, open_date, close_date, open_order_id, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stoploss_order_id, stoploss_last_update, - max_rate, min_rate, sell_reason, close_order_status, strategy, + max_rate, min_rate, sell_reason, sell_order_status, strategy, timeframe, open_trade_value, close_profit_abs, leverage, borrowed, borrowed_currency, collateral_currency, interest_rate, liquidation_price, is_short ) @@ -111,7 +112,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {initial_stop_loss_pct} initial_stop_loss_pct, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, - {close_order_status} close_order_status, + {sell_order_status} sell_order_status, {strategy} strategy, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, {leverage} leverage, {borrowed} borrowed, {borrowed_currency} borrowed_currency, @@ -120,7 +121,9 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col from {table_back_name} """)) -#TODO: Does leverage go in here? +# TODO: Does leverage go in here? + + def migrate_open_orders_to_trades(engine): with engine.begin() as connection: connection.execute(text(""" diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 7af71ec89..1dd9fefa3 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -132,7 +132,7 @@ class Order(_DECL_BASE): order_filled_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True) - leverage = Column(Float, nullable=True, default=0.0) + leverage = Column(Float, nullable=True, default=1.0) def __repr__(self): return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' @@ -258,12 +258,12 @@ class LocalTrade(): # Lowest price reached min_rate: float = 0.0 sell_reason: str = '' - close_order_status: str = '' + sell_order_status: str = '' strategy: str = '' timeframe: Optional[int] = None # Margin trading properties - leverage: Optional[float] = 0.0 + leverage: Optional[float] = 1.0 borrowed: float = 0.0 borrowed_currency: str = None collateral_currency: str = None @@ -287,6 +287,8 @@ class LocalTrade(): for key in kwargs: setattr(self, key, kwargs[key]) + if not self.is_short: + self.is_short = False self.recalc_open_trade_value() def __repr__(self): @@ -348,7 +350,7 @@ class LocalTrade(): 'profit_abs': self.close_profit_abs, 'sell_reason': self.sell_reason, - 'close_order_status': self.close_order_status, + 'sell_order_status': self.sell_order_status, 'stop_loss_abs': self.stop_loss, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, @@ -371,7 +373,7 @@ class LocalTrade(): 'collateral_currency': self.collateral_currency, 'interest_rate': self.interest_rate, 'liquidation_price': self.liquidation_price, - 'leverage': self.leverage, + 'is_short': self.is_short, 'open_order_id': self.open_order_id, } @@ -474,12 +476,12 @@ class LocalTrade(): self.recalc_open_trade_value() if self.is_open: payment = "SELL" if self.is_short else "BUY" - logger.info(f'{order_type.upper()}_{payment} order has been fulfilled for {self}.') + logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.') self.open_order_id = None elif order_type in ('market', 'limit') and self.is_closing_trade(order['side']): if self.is_open: payment = "BUY" if self.is_short else "SELL" - logger.info(f'{order_type.upper()}_{payment} order has been fulfilled for {self}.') + logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.') self.close(safe_value_fallback(order, 'average', 'price')) # TODO: Double check this elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): self.stoploss_order_id = None @@ -502,7 +504,7 @@ class LocalTrade(): self.close_profit = self.calc_profit_ratio() self.close_profit_abs = self.calc_profit() self.is_open = False - self.close_order_status = 'closed' + self.sell_order_status = 'closed' self.open_order_id = None if show_msg: logger.info( @@ -576,8 +578,18 @@ class LocalTrade(): close_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore fees = close_trade * Decimal(fee or self.fee_close) - #TODO: Interest rate could be hourly instead of daily - interest = ((Decimal(self.interest_rate) * Decimal(self.borrowed)) * Decimal((datetime.utcnow() - self.open_date).days)) or 0 # Interest/day * num of days + + # TODO: Need to set other conditions because sometimes self.open_date is not defined, but why would it ever not be set + try: + open = self.open_date.replace(tzinfo=None) + now = datetime.now() + + # breakpoint() + interest = ((Decimal(self.interest_rate or 0) * Decimal(self.borrowed or 0)) * + Decimal((now - open).total_seconds())/86400) or 0 # Interest/day * (seconds in trade)/(seconds per day) + except: + interest = 0 + if (self.is_short): return float(close_trade + fees + interest) else: @@ -617,12 +629,17 @@ class LocalTrade(): rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) - if self.open_trade_value == 0.0: - return 0.0 if self.is_short: - profit_ratio = (close_trade_value / self.open_trade_value) - 1 + if close_trade_value == 0.0: + return 0.0 + else: + profit_ratio = (self.open_trade_value / close_trade_value) - 1 + else: - profit_ratio = (self.open_trade_value / close_trade_value) - 1 + if self.open_trade_value == 0.0: + return 0.0 + else: + profit_ratio = (close_trade_value / self.open_trade_value) - 1 return float(f"{profit_ratio:.8f}") def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: @@ -672,7 +689,7 @@ class LocalTrade(): sel_trades = [trade for trade in sel_trades if trade.close_date and trade.close_date > close_date] - return sel_trades + return sel_trades @staticmethod def close_bt_trade(trade): @@ -768,13 +785,13 @@ class Trade(_DECL_BASE, LocalTrade): max_rate = Column(Float, nullable=True, default=0.0) # Lowest price reached min_rate = Column(Float, nullable=True) - sell_reason = Column(String(100), nullable=True) #TODO: Change to close_reason - close_order_status = Column(String(100), nullable=True) + sell_reason = Column(String(100), nullable=True) # TODO: Change to close_reason + sell_order_status = Column(String(100), nullable=True) strategy = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) # Margin trading properties - leverage = Column(Float, nullable=True, default=0.0) + leverage = Column(Float, nullable=True, default=1.0) borrowed = Column(Float, nullable=False, default=0.0) borrowed_currency = Column(Float, nullable=True) collateral_currency = Column(String(25), nullable=True) diff --git a/tests/conftest.py b/tests/conftest.py index fdd78094c..6fb6b6c0a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -221,6 +221,8 @@ def create_mock_trades(fee, use_db: bool = True): trade = mock_trade_6(fee) add_trade(trade) + # TODO-mg: Add margin trades + if use_db: Trade.query.session.flush() @@ -250,6 +252,7 @@ def patch_coingekko(mocker) -> None: @pytest.fixture(scope='function') def init_persistence(default_conf): init_db(default_conf['db_url'], default_conf['dry_run']) + # TODO-mg: margin with leverage and/or borrowed? @pytest.fixture(scope="function") @@ -812,7 +815,7 @@ def shitcoinmarkets(markets): "future": False, "active": True }, - }) + }) return shitmarkets @@ -914,18 +917,17 @@ def limit_sell_order_old(): @pytest.fixture def limit_buy_order_old_partial(): - return { - 'id': 'mocked_limit_buy_old_partial', - 'type': 'limit', - 'side': 'buy', - 'symbol': 'ETH/BTC', - 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), - 'price': 0.00001099, - 'amount': 90.99181073, - 'filled': 23.0, - 'remaining': 67.99181073, - 'status': 'open' - } + return {'id': 'mocked_limit_buy_old_partial', + 'type': 'limit', + 'side': 'buy', + 'symbol': 'ETH/BTC', + 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), + 'price': 0.00001099, + 'amount': 90.99181073, + 'filled': 23.0, + 'remaining': 67.99181073, + 'status': 'open' + } @pytest.fixture @@ -1769,6 +1771,7 @@ def rpc_balance(): 'used': 0.0 }, } + # TODO-mg: Add shorts and leverage? @pytest.fixture @@ -2084,3 +2087,95 @@ def saved_hyperopt_results(): ].total_seconds() return hyperopt_res + + +# * Margin Tests + +@pytest.fixture +def leveraged_fee(): + return + + +@pytest.fixture +def short_fee(): + return + + +@pytest.fixture +def ticker_short(): + return + + +@pytest.fixture +def ticker_exit_short_up(): + return + + +@pytest.fixture +def ticker_exit_short_down(): + return + + +@pytest.fixture +def leveraged_markets(): + return + + +@pytest.fixture(scope='function') +def limit_short_order_open(): + return + + +@pytest.fixture(scope='function') +def limit_short_order(limit_short_order_open): + return + + +@pytest.fixture(scope='function') +def market_short_order(): + return + + +@pytest.fixture +def market_short_exit_order(): + return + + +@pytest.fixture +def limit_short_order_old(): + return + + +@pytest.fixture +def limit_exit_short_order_old(): + return + + +@pytest.fixture +def limit_short_order_old_partial(): + return + + +@pytest.fixture +def limit_short_order_old_partial_canceled(limit_short_order_old_partial): + return + + +@pytest.fixture(scope='function') +def limit_short_order_canceled_empty(request): + return + + +@pytest.fixture +def limit_exit_short_order_open(): + return + + +@pytest.fixture +def limit_exit_short_order(limit_sell_order_open): + return + + +@pytest.fixture +def short_order_fee(): + return diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index b92b51144..de856a98d 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta, timezone from freqtrade.persistence.models import Order, Trade -MOCK_TRADE_COUNT = 6 +MOCK_TRADE_COUNT = 6 # TODO-mg: Increase for short and leverage def mock_order_1(): @@ -303,3 +303,5 @@ def mock_trade_6(fee): o = Order.parse_from_ccxt_object(mock_order_6_sell(), 'LTC/BTC', 'sell') trade.orders.append(o) return trade + +# TODO-mg: Mock orders for leveraged and short trades diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index fad24f9e2..50c1a0b31 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -107,6 +107,14 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', + + 'leverage': 1.0, + 'borrowed': 0.0, + 'borrowed_currency': None, + 'collateral_currency': None, + 'interest_rate': 0.0, + 'liquidation_price': None, + 'is_short': False, } mocker.patch('freqtrade.exchange.Exchange.get_rate', @@ -173,6 +181,15 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', + + 'leverage': 1.0, + 'borrowed': 0.0, + 'borrowed_currency': None, + 'collateral_currency': None, + 'interest_rate': 0.0, + 'liquidation_price': None, + 'is_short': False, + } diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 89d07ca74..e9441136b 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -129,6 +129,9 @@ def test_update_with_binance(limit_buy_order, limit_sell_order, fee, caplog): r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=.*\).", caplog) + # TODO-mg: create a short order + # TODO-mg: create a leveraged long order + @pytest.mark.usefixtures("init_persistence") def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): @@ -167,6 +170,9 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): r"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=.*\).", caplog) + # TODO-mg: market short + # TODO-mg: market leveraged long + @pytest.mark.usefixtures("init_persistence") def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): @@ -659,11 +665,13 @@ def test_migrate_new(mocker, default_conf, fee, caplog): order_date DATETIME, order_filled_date DATETIME, order_update_date DATETIME, + leverage FLOAT, PRIMARY KEY (id), CONSTRAINT _order_pair_order_id UNIQUE (ft_pair, order_id), FOREIGN KEY(ft_trade_id) REFERENCES trades (id) ) """)) + # TODO-mg: Had to add field leverage to this table, check that this is correct connection.execute(text(""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, @@ -912,6 +920,14 @@ def test_to_json(default_conf, fee): 'strategy': None, 'timeframe': None, 'exchange': 'binance', + + 'leverage': None, + 'borrowed': None, + 'borrowed_currency': None, + 'collateral_currency': None, + 'interest_rate': None, + 'liquidation_price': None, + 'is_short': None, } # Simulate dry_run entries @@ -977,6 +993,14 @@ def test_to_json(default_conf, fee): 'strategy': None, 'timeframe': None, 'exchange': 'binance', + + 'leverage': None, + 'borrowed': None, + 'borrowed_currency': None, + 'collateral_currency': None, + 'interest_rate': None, + 'liquidation_price': None, + 'is_short': None, } @@ -1315,11 +1339,11 @@ def test_Trade_object_idem(): 'get_overall_performance', 'get_total_closed_profit', 'total_open_trades_stakes', - 'get_sold_trades_without_assigned_fees', + 'get_closed_trades_without_assigned_fees', 'get_open_trades_without_assigned_fees', 'get_open_order_trades', 'get_trades', - ) + ) # Parent (LocalTrade) should have the same attributes for item in trade: From b6cc3f02bf230c88368941ca829b5eb350336696 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 22 Jun 2021 21:09:52 -0600 Subject: [PATCH 06/46] Created interest function --- freqtrade/persistence/models.py | 74 ++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 1dd9fefa3..26503f8c6 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -563,6 +563,42 @@ class LocalTrade(): """ self.open_trade_value = self._calc_open_trade_value() + 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) + + if self.exchange == 'binance': + # Rate is per day but accrued hourly or something + # binance: https://www.binance.com/en-AU/support/faq/360030157812 + return borrowed * (rate/24) * max(hours, 1.0) # TODO-mg: Is hours rounded? + 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 + return borrowed * (rate/24) * max(hours, 1.0) + elif self.exchange == 'binance_coinm_futures': + # ! TODO-mg: This is incorrect, I didn't look it up + return borrowed * (rate/24) * max(hours, 1.0) + else: + # TODO-mg: make sure this breaks and can't be squelched + raise OperationalException("Leverage not available on this exchange") + def calc_close_trade_value(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float: """ @@ -578,17 +614,7 @@ class LocalTrade(): close_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore fees = close_trade * Decimal(fee or self.fee_close) - - # TODO: Need to set other conditions because sometimes self.open_date is not defined, but why would it ever not be set - try: - open = self.open_date.replace(tzinfo=None) - now = datetime.now() - - # breakpoint() - interest = ((Decimal(self.interest_rate or 0) * Decimal(self.borrowed or 0)) * - Decimal((now - open).total_seconds())/86400) or 0 # Interest/day * (seconds in trade)/(seconds per day) - except: - interest = 0 + interest = self.calculate_interest() if (self.is_short): return float(close_trade + fees + interest) @@ -657,7 +683,7 @@ class LocalTrade(): else: return None - @staticmethod + @ staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, open_date: datetime = None, close_date: datetime = None, ) -> List['LocalTrade']: @@ -691,27 +717,27 @@ class LocalTrade(): return sel_trades - @staticmethod + @ staticmethod def close_bt_trade(trade): LocalTrade.trades_open.remove(trade) LocalTrade.trades.append(trade) LocalTrade.total_profit += trade.close_profit_abs - @staticmethod + @ staticmethod def add_bt_trade(trade): if trade.is_open: LocalTrade.trades_open.append(trade) else: LocalTrade.trades.append(trade) - @staticmethod + @ staticmethod def get_open_trades() -> List[Any]: """ Query trades from persistence layer """ return Trade.get_trades_proxy(is_open=True) - @staticmethod + @ staticmethod def stoploss_reinitialization(desired_stoploss): """ Adjust initial Stoploss to desired stoploss for all open trades. @@ -812,11 +838,11 @@ class Trade(_DECL_BASE, LocalTrade): Trade.query.session.delete(self) Trade.commit() - @staticmethod + @ staticmethod def commit(): Trade.query.session.commit() - @staticmethod + @ staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, open_date: datetime = None, close_date: datetime = None, ) -> List['LocalTrade']: @@ -846,7 +872,7 @@ class Trade(_DECL_BASE, LocalTrade): close_date=close_date ) - @staticmethod + @ staticmethod def get_trades(trade_filter=None) -> Query: """ Helper function to query Trades using filters. @@ -866,7 +892,7 @@ class Trade(_DECL_BASE, LocalTrade): else: return Trade.query - @staticmethod + @ staticmethod def get_open_order_trades(): """ Returns all open trades @@ -874,7 +900,7 @@ class Trade(_DECL_BASE, LocalTrade): """ return Trade.get_trades(Trade.open_order_id.isnot(None)).all() - @staticmethod + @ staticmethod def get_open_trades_without_assigned_fees(): """ Returns all open trades which don't have open fees set correctly @@ -885,7 +911,7 @@ class Trade(_DECL_BASE, LocalTrade): Trade.is_open.is_(True), ]).all() - @staticmethod + @ staticmethod def get_closed_trades_without_assigned_fees(): """ Returns all closed trades which don't have fees set correctly @@ -923,7 +949,7 @@ class Trade(_DECL_BASE, LocalTrade): t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True)) return total_open_stake_amount or 0 - @staticmethod + @ staticmethod def get_overall_performance() -> List[Dict[str, Any]]: """ Returns List of dicts containing all Trades, including profit and trade count @@ -986,7 +1012,7 @@ class PairLock(_DECL_BASE): return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, ' f'lock_end_time={lock_end_time})') - @staticmethod + @ staticmethod def query_pair_locks(pair: Optional[str], now: datetime) -> Query: """ Get all currently active locks for this pair From 692c55088a1df8501fe282ac63939ec69eab93d0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 22 Jun 2021 22:26:10 -0600 Subject: [PATCH 07/46] Started some pytests for short and leverage 1 short test passes --- freqtrade/persistence/models.py | 31 +- tests/conftest.py | 163 ++++----- tests/conftest_trades.py | 131 ++++++- tests/test_persistence.py | 26 +- tests/test_persistence_margin.py | 596 +++++++++++++++++++++++++++++++ 5 files changed, 809 insertions(+), 138 deletions(-) create mode 100644 tests/test_persistence_margin.py diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 26503f8c6..811b7d1f8 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -133,6 +133,7 @@ class Order(_DECL_BASE): order_update_date = Column(DateTime, nullable=True) leverage = Column(Float, nullable=True, default=1.0) + is_short = Column(Boolean, nullable=False, default=False) def __repr__(self): return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' @@ -447,14 +448,16 @@ class LocalTrade(): Determines if the trade is an opening (long buy or short sell) trade :param side (string): the side (buy/sell) that order happens on """ - return (side == 'buy' and not self.is_short) or (side == 'sell' and self.is_short) + is_short = self.is_short + return (side == 'buy' and not is_short) or (side == 'sell' and is_short) 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 """ - return (side == 'sell' and not self.is_short) or (side == 'buy' and self.is_short) + is_short = self.is_short + return (side == 'sell' and not is_short) or (side == 'buy' and is_short) def update(self, order: Dict) -> None: """ @@ -463,6 +466,9 @@ class LocalTrade(): :return: None """ order_type = order['type'] + # 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'] # Ignore open and cancelled orders if order['status'] == 'open' or safe_value_fallback(order, 'average', 'price') is None: return @@ -579,11 +585,13 @@ class LocalTrade(): rate = Decimal(self.interest_rate) borrowed = Decimal(self.borrowed) + twenty4 = Decimal(24.0) + one = Decimal(1.0) if self.exchange == 'binance': # Rate is per day but accrued hourly or something # binance: https://www.binance.com/en-AU/support/faq/360030157812 - return borrowed * (rate/24) * max(hours, 1.0) # TODO-mg: Is hours rounded? + return borrowed * (rate/twenty4) * max(hours, one) # TODO-mg: Is hours rounded? elif self.exchange == 'kraken': # https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading- opening_fee = borrowed * rate @@ -591,10 +599,10 @@ class LocalTrade(): return opening_fee + roll_over_fee elif self.exchange == 'binance_usdm_futures': # ! TODO-mg: This is incorrect, I didn't look it up - return borrowed * (rate/24) * max(hours, 1.0) + return borrowed * (rate/twenty4) * max(hours, one) elif self.exchange == 'binance_coinm_futures': # ! TODO-mg: This is incorrect, I didn't look it up - return borrowed * (rate/24) * max(hours, 1.0) + return borrowed * (rate/twenty4) * max(hours, one) else: # TODO-mg: make sure this breaks and can't be squelched raise OperationalException("Leverage not available on this exchange") @@ -612,14 +620,19 @@ class LocalTrade(): if rate is None and not self.close_rate: return 0.0 - close_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore - fees = close_trade * Decimal(fee or self.fee_close) interest = self.calculate_interest() + 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) if (self.is_short): - return float(close_trade + fees + interest) + return float(close_trade + fees) else: - return float(close_trade - fees - interest) + return float(close_trade - fees) def calc_profit(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float: diff --git a/tests/conftest.py b/tests/conftest.py index 6fb6b6c0a..a78dd2bc2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,12 +7,10 @@ from datetime import datetime, timedelta from functools import reduce from pathlib import Path from unittest.mock import MagicMock, Mock, PropertyMock - import arrow import numpy as np import pytest from telegram import Chat, Message, Update - from freqtrade import constants from freqtrade.commands import Arguments from freqtrade.data.converter import ohlcv_to_dataframe @@ -24,12 +22,8 @@ from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, - mock_trade_5, mock_trade_6) - - + mock_trade_5, mock_trade_6, short_trade, leverage_trade) logging.getLogger('').setLevel(logging.INFO) - - # Do not mask numpy errors as warnings that no one read, raise the exсeption np.seterr(all='raise') @@ -63,13 +57,12 @@ def log_has_re(line, logs): def get_args(args): return Arguments(args).get_parsed_arg() - - # Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines + + def get_mock_coro(return_value): async def mock_coro(*args, **kwargs): return return_value - return Mock(wraps=mock_coro) @@ -92,7 +85,6 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No if mock_markets: mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=get_markets())) - if api_mock: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) else: @@ -126,7 +118,6 @@ def patch_edge(mocker) -> None: # "LTC/BTC", # "XRP/BTC", # "NEO/BTC" - mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( return_value={ 'NEO/BTC': PairInfo(-0.20, 0.66, 3.71, 0.50, 1.71, 10, 25), @@ -140,7 +131,6 @@ def get_patched_edge(mocker, config) -> Edge: patch_edge(mocker) edge = Edge(config) return edge - # Functions for recurrent object patching @@ -201,28 +191,24 @@ def create_mock_trades(fee, use_db: bool = True): Trade.query.session.add(trade) else: LocalTrade.add_bt_trade(trade) - # Simulate dry_run entries trade = mock_trade_1(fee) add_trade(trade) - trade = mock_trade_2(fee) add_trade(trade) - trade = mock_trade_3(fee) add_trade(trade) - trade = mock_trade_4(fee) add_trade(trade) - trade = mock_trade_5(fee) add_trade(trade) - trade = mock_trade_6(fee) add_trade(trade) - - # TODO-mg: Add margin trades - + # TODO: margin trades + # trade = short_trade(fee) + # add_trade(trade) + # trade = leverage_trade(fee) + # add_trade(trade) if use_db: Trade.query.session.flush() @@ -234,7 +220,6 @@ def patch_coingekko(mocker) -> None: :param mocker: mocker to patch coingekko class :return: None """ - tickermock = MagicMock(return_value={'bitcoin': {'usd': 12345.0}, 'ethereum': {'usd': 12345.0}}) listmock = MagicMock(return_value=[{'id': 'bitcoin', 'name': 'Bitcoin', 'symbol': 'btc', 'website_slug': 'bitcoin'}, @@ -245,14 +230,13 @@ def patch_coingekko(mocker) -> None: 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', get_price=tickermock, get_coins_list=listmock, - ) @pytest.fixture(scope='function') def init_persistence(default_conf): init_db(default_conf['db_url'], default_conf['dry_run']) - # TODO-mg: margin with leverage and/or borrowed? + # TODO-mg: trade with leverage and/or borrowed? @pytest.fixture(scope="function") @@ -943,7 +927,6 @@ def limit_buy_order_canceled_empty(request): # Indirect fixture # Documentation: # https://docs.pytest.org/en/latest/example/parametrize.html#apply-indirect-on-particular-arguments - exchange_name = request.param if exchange_name == 'ftx': return { @@ -1733,7 +1716,6 @@ def edge_conf(default_conf): "max_trade_duration_minute": 1440, "remove_pumps": False } - return conf @@ -1791,12 +1773,9 @@ def import_fails() -> None: if name in ["filelock", 'systemd.journal', 'uvloop']: raise ImportError(f"No module named '{name}'") return realimport(name, *args, **kwargs) - builtins.__import__ = mockedimport - # Run test - then cleanup yield - # restore previous importfunction builtins.__import__ = realimport @@ -2081,101 +2060,79 @@ def saved_hyperopt_results(): 'is_best': False } ] - for res in hyperopt_res: res['results_metrics']['holding_avg_s'] = res['results_metrics']['holding_avg' ].total_seconds() - return hyperopt_res - # * Margin Tests + @pytest.fixture -def leveraged_fee(): - return +def ten_minutes_ago(): + return datetime.utcnow() - timedelta(hours=0, minutes=10) @pytest.fixture -def short_fee(): - return - - -@pytest.fixture -def ticker_short(): - return - - -@pytest.fixture -def ticker_exit_short_up(): - return - - -@pytest.fixture -def ticker_exit_short_down(): - return - - -@pytest.fixture -def leveraged_markets(): - return +def five_hours_ago(): + return datetime.utcnow() - timedelta(hours=1, minutes=0) @pytest.fixture(scope='function') def limit_short_order_open(): - return - - -@pytest.fixture(scope='function') -def limit_short_order(limit_short_order_open): - return - - -@pytest.fixture(scope='function') -def market_short_order(): - return - - -@pytest.fixture -def market_short_exit_order(): - return - - -@pytest.fixture -def limit_short_order_old(): - return - - -@pytest.fixture -def limit_exit_short_order_old(): - return - - -@pytest.fixture -def limit_short_order_old_partial(): - return - - -@pytest.fixture -def limit_short_order_old_partial_canceled(limit_short_order_old_partial): - return - - -@pytest.fixture(scope='function') -def limit_short_order_canceled_empty(request): - return + return { + 'id': 'mocked_limit_short', + 'type': 'limit', + 'side': 'sell', + 'symbol': 'mocked', + 'datetime': arrow.utcnow().isoformat(), + 'timestamp': arrow.utcnow().int_timestamp, + 'price': 0.00001173, + 'amount': 90.99181073, + 'borrowed': 90.99181073, + 'filled': 0.0, + 'cost': 0.00106733393, + 'remaining': 90.99181073, + 'status': 'open', + 'is_short': True + } @pytest.fixture def limit_exit_short_order_open(): - return + return { + 'id': 'mocked_limit_exit_short', + 'type': 'limit', + 'side': 'buy', + 'pair': 'mocked', + 'datetime': arrow.utcnow().isoformat(), + 'timestamp': arrow.utcnow().int_timestamp, + 'price': 0.00001099, + 'amount': 90.99181073, + 'filled': 0.0, + 'remaining': 90.99181073, + 'status': 'open' + } + + +@pytest.fixture(scope='function') +def limit_short_order(limit_short_order_open): + order = deepcopy(limit_short_order_open) + order['status'] = 'closed' + order['filled'] = order['amount'] + order['remaining'] = 0.0 + return order @pytest.fixture -def limit_exit_short_order(limit_sell_order_open): - return +def limit_exit_short_order(limit_exit_short_order_open): + order = deepcopy(limit_exit_short_order_open) + order['remaining'] = 0.0 + order['filled'] = order['amount'] + order['status'] = 'closed' + return order @pytest.fixture -def short_order_fee(): - return +def interest_rate(): + return MagicMock(return_value=0.0005) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index de856a98d..2aa1d6b4c 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -304,4 +304,133 @@ def mock_trade_6(fee): trade.orders.append(o) return trade -# TODO-mg: Mock orders for leveraged and short trades + +#! TODO Currently the following short_trade test and leverage_trade test will fail + + +def short_order(): + return { + 'id': '1235', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'sell', + 'type': 'limit', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + 'leverage': 5.0, + 'isShort': True + } + + +def exit_short_order(): + return { + 'id': '12366', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'buy', + 'type': 'limit', + 'price': 0.128, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + 'leverage': 5.0, + 'isShort': True + } + + +def short_trade(fee): + """ + Closed trade... + """ + trade = Trade( + pair='ETC/BTC', + stake_amount=0.001, + amount=123.0, # TODO-mg: In BTC? + amount_requested=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + close_rate=0.128, + close_profit=0.005, # TODO-mg: Would this be -0.005 or -0.025 + close_profit_abs=0.000584127, + exchange='binance', + is_open=False, + open_order_id='dry_run_exit_short_12345', + strategy='DefaultStrategy', + timeframe=5, + sell_reason='sell_signal', # TODO-mg: Update to exit/close reason + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), + close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2), + # borrowed= + isShort=True + ) + o = Order.parse_from_ccxt_object(short_order(), 'ETC/BTC', 'sell') + trade.orders.append(o) + o = Order.parse_from_ccxt_object(exit_short_order(), 'ETC/BTC', 'sell') + trade.orders.append(o) + return trade + + +def leverage_order(): + return { + 'id': '1235', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'buy', + 'type': 'limit', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + 'leverage': 5.0 + } + + +def leverage_order_sell(): + return { + 'id': '12366', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'sell', + 'type': 'limit', + 'price': 0.128, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + 'leverage': 5.0, + 'isShort': True + } + + +def leverage_trade(fee): + """ + Closed trade... + """ + trade = Trade( + pair='ETC/BTC', + stake_amount=0.001, + amount=615.0, + amount_requested=615.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + close_rate=0.128, + close_profit=0.005, # TODO-mg: Would this be -0.005 or -0.025 + close_profit_abs=0.000584127, + exchange='binance', + is_open=False, + open_order_id='dry_run_leverage_sell_12345', + strategy='DefaultStrategy', + timeframe=5, + sell_reason='sell_signal', # TODO-mg: Update to exit/close reason + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), + close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2), + # borrowed= + ) + o = Order.parse_from_ccxt_object(leverage_order(), 'ETC/BTC', 'sell') + trade.orders.append(o) + o = Order.parse_from_ccxt_object(leverage_order_sell(), 'ETC/BTC', 'sell') + trade.orders.append(o) + return trade diff --git a/tests/test_persistence.py b/tests/test_persistence.py index e9441136b..f4494e967 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -129,9 +129,6 @@ def test_update_with_binance(limit_buy_order, limit_sell_order, fee, caplog): r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=.*\).", caplog) - # TODO-mg: create a short order - # TODO-mg: create a leveraged long order - @pytest.mark.usefixtures("init_persistence") def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): @@ -170,9 +167,6 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): r"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=.*\).", caplog) - # TODO-mg: market short - # TODO-mg: market leveraged long - @pytest.mark.usefixtures("init_persistence") def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): @@ -665,13 +659,11 @@ def test_migrate_new(mocker, default_conf, fee, caplog): order_date DATETIME, order_filled_date DATETIME, order_update_date DATETIME, - leverage FLOAT, PRIMARY KEY (id), CONSTRAINT _order_pair_order_id UNIQUE (ft_pair, order_id), FOREIGN KEY(ft_trade_id) REFERENCES trades (id) ) """)) - # TODO-mg: Had to add field leverage to this table, check that this is correct connection.execute(text(""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, @@ -920,14 +912,6 @@ def test_to_json(default_conf, fee): 'strategy': None, 'timeframe': None, 'exchange': 'binance', - - 'leverage': None, - 'borrowed': None, - 'borrowed_currency': None, - 'collateral_currency': None, - 'interest_rate': None, - 'liquidation_price': None, - 'is_short': None, } # Simulate dry_run entries @@ -993,14 +977,6 @@ def test_to_json(default_conf, fee): 'strategy': None, 'timeframe': None, 'exchange': 'binance', - - 'leverage': None, - 'borrowed': None, - 'borrowed_currency': None, - 'collateral_currency': None, - 'interest_rate': None, - 'liquidation_price': None, - 'is_short': None, } @@ -1339,7 +1315,7 @@ def test_Trade_object_idem(): 'get_overall_performance', 'get_total_closed_profit', 'total_open_trades_stakes', - 'get_closed_trades_without_assigned_fees', + 'get_sold_trades_without_assigned_fees', 'get_open_trades_without_assigned_fees', 'get_open_order_trades', 'get_trades', diff --git a/tests/test_persistence_margin.py b/tests/test_persistence_margin.py new file mode 100644 index 000000000..d31bde590 --- /dev/null +++ b/tests/test_persistence_margin.py @@ -0,0 +1,596 @@ +import logging +from datetime import datetime, timedelta, timezone +from pathlib import Path +from types import FunctionType +from unittest.mock import MagicMock +import arrow +import pytest +from sqlalchemy import create_engine, inspect, text +from freqtrade import constants +from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db +from tests.conftest import create_mock_trades, log_has, log_has_re + +# * Margin tests + + +@pytest.mark.usefixtures("init_persistence") +def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, interest_rate, ten_minutes_ago, caplog): + """ + On this test we will short and buy back(exit short) a crypto currency at 1x leverage + #*The actual program uses more precise numbers + Short + - Sell: 90.99181073 Crypto at 0.00001173 BTC + - Selling fee: 0.25% + - Total value of sell trade: 0.001064666 BTC + ((90.99181073*0.00001173) - ((90.99181073*0.00001173)*0.0025)) + Exit Short + - Buy: 90.99181073 Crypto at 0.00001099 BTC + - Buying fee: 0.25% + - Interest fee: 0.05% + - Total interest + (90.99181073 * 0.0005)/24 = 0.00189566272 + - Total cost of buy trade: 0.00100252088 + (90.99181073 + 0.00189566272) * 0.00001099 = 0.00100002083 :(borrowed + interest * cost) + + ((90.99181073 + 0.00189566272)*0.00001099)*0.0025 = 0.00000250005 + = 0.00100252088 + + Profit/Loss: +0.00006214512 BTC + Sell:0.001064666 - Buy:0.00100252088 + Profit/Loss percentage: 0.06198885353 + (0.001064666/0.00100252088)-1 = 0.06198885353 + #* ~0.061988453889463014104555743 With more precise numbers used + :param limit_short_order: + :param limit_exit_short_order: + :param fee + :param interest_rate + :param caplog + :return: + """ + trade = Trade( + id=2, + pair='ETH/BTC', + stake_amount=0.001, + open_rate=0.01, + amount=5, + is_open=True, + open_date=ten_minutes_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + interest_rate=interest_rate.return_value, + borrowed=90.99181073, + exchange='binance', + is_short=True + ) + #assert trade.open_order_id is None + assert trade.close_profit is None + assert trade.close_date is None + #trade.open_order_id = 'something' + trade.update(limit_short_order) + #assert trade.open_order_id is None + assert trade.open_rate == 0.00001173 + assert trade.close_profit is None + assert trade.close_date is None + assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, " + r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001173, open_since=.*\).", + caplog) + caplog.clear() + #trade.open_order_id = 'something' + trade.update(limit_exit_short_order) + #assert trade.open_order_id is None + assert trade.close_rate == 0.00001099 + assert trade.close_profit == 0.06198845 + assert trade.close_date is not None + assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, " + r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001173, open_since=.*\).", + caplog) + + # TODO-mg: create a leveraged long order + + +# @pytest.mark.usefixtures("init_persistence") +# def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): +# trade = Trade( +# id=1, +# pair='ETH/BTC', +# stake_amount=0.001, +# amount=5, +# open_rate=0.01, +# is_open=True, +# fee_open=fee.return_value, +# fee_close=fee.return_value, +# open_date=arrow.utcnow().datetime, +# exchange='binance', +# ) +# trade.open_order_id = 'something' +# trade.update(market_buy_order) +# assert trade.open_order_id is None +# assert trade.open_rate == 0.00004099 +# assert trade.close_profit is None +# assert trade.close_date is None +# assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, " +# r"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=.*\).", +# caplog) +# caplog.clear() +# trade.is_open = True +# trade.open_order_id = 'something' +# trade.update(market_sell_order) +# assert trade.open_order_id is None +# assert trade.close_rate == 0.00004173 +# assert trade.close_profit == 0.01297561 +# assert trade.close_date is not None +# assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " +# r"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=.*\).", +# caplog) +# # TODO-mg: market short +# # TODO-mg: market leveraged long + + +# @pytest.mark.usefixtures("init_persistence") +# def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): +# trade = Trade( +# pair='ETH/BTC', +# stake_amount=0.001, +# open_rate=0.01, +# amount=5, +# fee_open=fee.return_value, +# fee_close=fee.return_value, +# exchange='binance', +# ) +# trade.open_order_id = 'something' +# trade.update(limit_buy_order) +# assert trade._calc_open_trade_value() == 0.0010024999999225068 +# trade.update(limit_sell_order) +# assert trade.calc_close_trade_value() == 0.0010646656050132426 +# # Profit in BTC +# assert trade.calc_profit() == 0.00006217 +# # Profit in percent +# assert trade.calc_profit_ratio() == 0.06201058 + + +# @pytest.mark.usefixtures("init_persistence") +# def test_trade_close(limit_buy_order, limit_sell_order, fee): +# trade = Trade( +# pair='ETH/BTC', +# stake_amount=0.001, +# open_rate=0.01, +# amount=5, +# is_open=True, +# fee_open=fee.return_value, +# fee_close=fee.return_value, +# open_date=arrow.Arrow(2020, 2, 1, 15, 5, 1).datetime, +# exchange='binance', +# ) +# assert trade.close_profit is None +# assert trade.close_date is None +# assert trade.is_open is True +# trade.close(0.02) +# assert trade.is_open is False +# assert trade.close_profit == 0.99002494 +# assert trade.close_date is not None +# new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, +# assert trade.close_date != new_date +# # Close should NOT update close_date if the trade has been closed already +# assert trade.is_open is False +# trade.close_date = new_date +# trade.close(0.02) +# assert trade.close_date == new_date + + +# @pytest.mark.usefixtures("init_persistence") +# def test_calc_close_trade_price_exception(limit_buy_order, fee): +# trade = Trade( +# pair='ETH/BTC', +# stake_amount=0.001, +# open_rate=0.1, +# amount=5, +# fee_open=fee.return_value, +# fee_close=fee.return_value, +# exchange='binance', +# ) +# trade.open_order_id = 'something' +# trade.update(limit_buy_order) +# assert trade.calc_close_trade_value() == 0.0 + + +# @pytest.mark.usefixtures("init_persistence") +# def test_update_open_order(limit_buy_order): +# trade = Trade( +# pair='ETH/BTC', +# stake_amount=1.00, +# open_rate=0.01, +# amount=5, +# fee_open=0.1, +# fee_close=0.1, +# exchange='binance', +# ) +# assert trade.open_order_id is None +# assert trade.close_profit is None +# assert trade.close_date is None +# limit_buy_order['status'] = 'open' +# trade.update(limit_buy_order) +# assert trade.open_order_id is None +# assert trade.close_profit is None +# assert trade.close_date is None + + +# @pytest.mark.usefixtures("init_persistence") +# def test_calc_open_trade_value(limit_buy_order, fee): +# trade = Trade( +# pair='ETH/BTC', +# stake_amount=0.001, +# amount=5, +# open_rate=0.00001099, +# fee_open=fee.return_value, +# fee_close=fee.return_value, +# exchange='binance', +# ) +# trade.open_order_id = 'open_trade' +# trade.update(limit_buy_order) # Buy @ 0.00001099 +# # Get the open rate price with the standard fee rate +# assert trade._calc_open_trade_value() == 0.0010024999999225068 +# trade.fee_open = 0.003 +# # Get the open rate price with a custom fee rate +# assert trade._calc_open_trade_value() == 0.001002999999922468 + + +# @pytest.mark.usefixtures("init_persistence") +# def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee): +# trade = Trade( +# pair='ETH/BTC', +# stake_amount=0.001, +# amount=5, +# open_rate=0.00001099, +# fee_open=fee.return_value, +# fee_close=fee.return_value, +# exchange='binance', +# ) +# trade.open_order_id = 'close_trade' +# trade.update(limit_buy_order) # Buy @ 0.00001099 +# # Get the close rate price with a custom close rate and a regular fee rate +# assert trade.calc_close_trade_value(rate=0.00001234) == 0.0011200318470471794 +# # Get the close rate price with a custom close rate and a custom fee rate +# assert trade.calc_close_trade_value(rate=0.00001234, fee=0.003) == 0.0011194704275749754 +# # Test when we apply a Sell order, and ask price with a custom fee rate +# trade.update(limit_sell_order) +# assert trade.calc_close_trade_value(fee=0.005) == 0.0010619972701635854 + + +# @pytest.mark.usefixtures("init_persistence") +# def test_calc_profit(limit_buy_order, limit_sell_order, fee): +# trade = Trade( +# pair='ETH/BTC', +# stake_amount=0.001, +# amount=5, +# open_rate=0.00001099, +# fee_open=fee.return_value, +# fee_close=fee.return_value, +# exchange='binance', +# ) +# trade.open_order_id = 'something' +# trade.update(limit_buy_order) # Buy @ 0.00001099 +# # Custom closing rate and regular fee rate +# # Higher than open rate +# assert trade.calc_profit(rate=0.00001234) == 0.00011753 +# # Lower than open rate +# assert trade.calc_profit(rate=0.00000123) == -0.00089086 +# # Custom closing rate and custom fee rate +# # Higher than open rate +# assert trade.calc_profit(rate=0.00001234, fee=0.003) == 0.00011697 +# # Lower than open rate +# assert trade.calc_profit(rate=0.00000123, fee=0.003) == -0.00089092 +# # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 +# trade.update(limit_sell_order) +# assert trade.calc_profit() == 0.00006217 +# # Test with a custom fee rate on the close trade +# assert trade.calc_profit(fee=0.003) == 0.00006163 + + +# @pytest.mark.usefixtures("init_persistence") +# def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee): +# trade = Trade( +# pair='ETH/BTC', +# stake_amount=0.001, +# amount=5, +# open_rate=0.00001099, +# fee_open=fee.return_value, +# fee_close=fee.return_value, +# exchange='binance', +# ) +# trade.open_order_id = 'something' +# trade.update(limit_buy_order) # Buy @ 0.00001099 +# # Get percent of profit with a custom rate (Higher than open rate) +# assert trade.calc_profit_ratio(rate=0.00001234) == 0.11723875 +# # Get percent of profit with a custom rate (Lower than open rate) +# assert trade.calc_profit_ratio(rate=0.00000123) == -0.88863828 +# # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 +# trade.update(limit_sell_order) +# assert trade.calc_profit_ratio() == 0.06201058 +# # Test with a custom fee rate on the close trade +# assert trade.calc_profit_ratio(fee=0.003) == 0.06147824 +# trade.open_trade_value = 0.0 +# assert trade.calc_profit_ratio(fee=0.003) == 0.0 + + +# def test_adjust_stop_loss(fee): +# trade = Trade( +# pair='ETH/BTC', +# stake_amount=0.001, +# amount=5, +# fee_open=fee.return_value, +# fee_close=fee.return_value, +# exchange='binance', +# open_rate=1, +# max_rate=1, +# ) +# trade.adjust_stop_loss(trade.open_rate, 0.05, True) +# assert trade.stop_loss == 0.95 +# assert trade.stop_loss_pct == -0.05 +# assert trade.initial_stop_loss == 0.95 +# assert trade.initial_stop_loss_pct == -0.05 +# # Get percent of profit with a lower rate +# trade.adjust_stop_loss(0.96, 0.05) +# assert trade.stop_loss == 0.95 +# assert trade.stop_loss_pct == -0.05 +# assert trade.initial_stop_loss == 0.95 +# assert trade.initial_stop_loss_pct == -0.05 +# # Get percent of profit with a custom rate (Higher than open rate) +# trade.adjust_stop_loss(1.3, -0.1) +# assert round(trade.stop_loss, 8) == 1.17 +# assert trade.stop_loss_pct == -0.1 +# assert trade.initial_stop_loss == 0.95 +# assert trade.initial_stop_loss_pct == -0.05 +# # current rate lower again ... should not change +# trade.adjust_stop_loss(1.2, 0.1) +# assert round(trade.stop_loss, 8) == 1.17 +# assert trade.initial_stop_loss == 0.95 +# assert trade.initial_stop_loss_pct == -0.05 +# # current rate higher... should raise stoploss +# trade.adjust_stop_loss(1.4, 0.1) +# assert round(trade.stop_loss, 8) == 1.26 +# assert trade.initial_stop_loss == 0.95 +# assert trade.initial_stop_loss_pct == -0.05 +# # Initial is true but stop_loss set - so doesn't do anything +# trade.adjust_stop_loss(1.7, 0.1, True) +# assert round(trade.stop_loss, 8) == 1.26 +# assert trade.initial_stop_loss == 0.95 +# assert trade.initial_stop_loss_pct == -0.05 +# assert trade.stop_loss_pct == -0.1 + + +# def test_adjust_min_max_rates(fee): +# trade = Trade( +# pair='ETH/BTC', +# stake_amount=0.001, +# amount=5, +# fee_open=fee.return_value, +# fee_close=fee.return_value, +# exchange='binance', +# open_rate=1, +# ) +# trade.adjust_min_max_rates(trade.open_rate) +# assert trade.max_rate == 1 +# assert trade.min_rate == 1 +# # check min adjusted, max remained +# trade.adjust_min_max_rates(0.96) +# assert trade.max_rate == 1 +# assert trade.min_rate == 0.96 +# # check max adjusted, min remains +# trade.adjust_min_max_rates(1.05) +# assert trade.max_rate == 1.05 +# assert trade.min_rate == 0.96 +# # current rate "in the middle" - no adjustment +# trade.adjust_min_max_rates(1.03) +# assert trade.max_rate == 1.05 +# assert trade.min_rate == 0.96 + + +# @pytest.mark.usefixtures("init_persistence") +# @pytest.mark.parametrize('use_db', [True, False]) +# def test_get_open(fee, use_db): +# Trade.use_db = use_db +# Trade.reset_trades() +# create_mock_trades(fee, use_db) +# assert len(Trade.get_open_trades()) == 4 +# Trade.use_db = True + + +# def test_stoploss_reinitialization(default_conf, fee): +# init_db(default_conf['db_url']) +# trade = Trade( +# pair='ETH/BTC', +# stake_amount=0.001, +# fee_open=fee.return_value, +# open_date=arrow.utcnow().shift(hours=-2).datetime, +# amount=10, +# fee_close=fee.return_value, +# exchange='binance', +# open_rate=1, +# max_rate=1, +# ) +# trade.adjust_stop_loss(trade.open_rate, 0.05, True) +# assert trade.stop_loss == 0.95 +# assert trade.stop_loss_pct == -0.05 +# assert trade.initial_stop_loss == 0.95 +# assert trade.initial_stop_loss_pct == -0.05 +# Trade.query.session.add(trade) +# # Lower stoploss +# Trade.stoploss_reinitialization(0.06) +# trades = Trade.get_open_trades() +# assert len(trades) == 1 +# trade_adj = trades[0] +# assert trade_adj.stop_loss == 0.94 +# assert trade_adj.stop_loss_pct == -0.06 +# assert trade_adj.initial_stop_loss == 0.94 +# assert trade_adj.initial_stop_loss_pct == -0.06 +# # Raise stoploss +# Trade.stoploss_reinitialization(0.04) +# trades = Trade.get_open_trades() +# assert len(trades) == 1 +# trade_adj = trades[0] +# assert trade_adj.stop_loss == 0.96 +# assert trade_adj.stop_loss_pct == -0.04 +# assert trade_adj.initial_stop_loss == 0.96 +# assert trade_adj.initial_stop_loss_pct == -0.04 +# # Trailing stoploss (move stoplos up a bit) +# trade.adjust_stop_loss(1.02, 0.04) +# assert trade_adj.stop_loss == 0.9792 +# assert trade_adj.initial_stop_loss == 0.96 +# Trade.stoploss_reinitialization(0.04) +# trades = Trade.get_open_trades() +# assert len(trades) == 1 +# trade_adj = trades[0] +# # Stoploss should not change in this case. +# assert trade_adj.stop_loss == 0.9792 +# assert trade_adj.stop_loss_pct == -0.04 +# assert trade_adj.initial_stop_loss == 0.96 +# assert trade_adj.initial_stop_loss_pct == -0.04 + + +# def test_update_fee(fee): +# trade = Trade( +# pair='ETH/BTC', +# stake_amount=0.001, +# fee_open=fee.return_value, +# open_date=arrow.utcnow().shift(hours=-2).datetime, +# amount=10, +# fee_close=fee.return_value, +# exchange='binance', +# open_rate=1, +# max_rate=1, +# ) +# fee_cost = 0.15 +# fee_currency = 'BTC' +# fee_rate = 0.0075 +# assert trade.fee_open_currency is None +# assert not trade.fee_updated('buy') +# assert not trade.fee_updated('sell') +# trade.update_fee(fee_cost, fee_currency, fee_rate, 'buy') +# assert trade.fee_updated('buy') +# assert not trade.fee_updated('sell') +# assert trade.fee_open_currency == fee_currency +# assert trade.fee_open_cost == fee_cost +# assert trade.fee_open == fee_rate +# # Setting buy rate should "guess" close rate +# assert trade.fee_close == fee_rate +# assert trade.fee_close_currency is None +# assert trade.fee_close_cost is None +# fee_rate = 0.0076 +# trade.update_fee(fee_cost, fee_currency, fee_rate, 'sell') +# assert trade.fee_updated('buy') +# assert trade.fee_updated('sell') +# assert trade.fee_close == 0.0076 +# assert trade.fee_close_cost == fee_cost +# assert trade.fee_close == fee_rate + + +# def test_fee_updated(fee): +# trade = Trade( +# pair='ETH/BTC', +# stake_amount=0.001, +# fee_open=fee.return_value, +# open_date=arrow.utcnow().shift(hours=-2).datetime, +# amount=10, +# fee_close=fee.return_value, +# exchange='binance', +# open_rate=1, +# max_rate=1, +# ) +# assert trade.fee_open_currency is None +# assert not trade.fee_updated('buy') +# assert not trade.fee_updated('sell') +# assert not trade.fee_updated('asdf') +# trade.update_fee(0.15, 'BTC', 0.0075, 'buy') +# assert trade.fee_updated('buy') +# assert not trade.fee_updated('sell') +# assert trade.fee_open_currency is not None +# assert trade.fee_close_currency is None +# trade.update_fee(0.15, 'ABC', 0.0075, 'sell') +# assert trade.fee_updated('buy') +# assert trade.fee_updated('sell') +# assert not trade.fee_updated('asfd') + + +# @pytest.mark.usefixtures("init_persistence") +# @pytest.mark.parametrize('use_db', [True, False]) +# def test_total_open_trades_stakes(fee, use_db): +# Trade.use_db = use_db +# Trade.reset_trades() +# res = Trade.total_open_trades_stakes() +# assert res == 0 +# create_mock_trades(fee, use_db) +# res = Trade.total_open_trades_stakes() +# assert res == 0.004 +# Trade.use_db = True + + +# @pytest.mark.usefixtures("init_persistence") +# def test_get_overall_performance(fee): +# create_mock_trades(fee) +# res = Trade.get_overall_performance() +# assert len(res) == 2 +# assert 'pair' in res[0] +# assert 'profit' in res[0] +# assert 'count' in res[0] + + +# @pytest.mark.usefixtures("init_persistence") +# def test_get_best_pair(fee): +# res = Trade.get_best_pair() +# assert res is None +# create_mock_trades(fee) +# res = Trade.get_best_pair() +# assert len(res) == 2 +# assert res[0] == 'XRP/BTC' +# assert res[1] == 0.01 + + +# @pytest.mark.usefixtures("init_persistence") +# def test_update_order_from_ccxt(caplog): +# # Most basic order return (only has orderid) +# o = Order.parse_from_ccxt_object({'id': '1234'}, 'ETH/BTC', 'buy') +# assert isinstance(o, Order) +# assert o.ft_pair == 'ETH/BTC' +# assert o.ft_order_side == 'buy' +# assert o.order_id == '1234' +# assert o.ft_is_open +# ccxt_order = { +# 'id': '1234', +# 'side': 'buy', +# 'symbol': 'ETH/BTC', +# 'type': 'limit', +# 'price': 1234.5, +# 'amount': 20.0, +# 'filled': 9, +# 'remaining': 11, +# 'status': 'open', +# 'timestamp': 1599394315123 +# } +# o = Order.parse_from_ccxt_object(ccxt_order, 'ETH/BTC', 'buy') +# assert isinstance(o, Order) +# assert o.ft_pair == 'ETH/BTC' +# assert o.ft_order_side == 'buy' +# assert o.order_id == '1234' +# assert o.order_type == 'limit' +# assert o.price == 1234.5 +# assert o.filled == 9 +# assert o.remaining == 11 +# assert o.order_date is not None +# assert o.ft_is_open +# assert o.order_filled_date is None +# # Order has been closed +# ccxt_order.update({'filled': 20.0, 'remaining': 0.0, 'status': 'closed'}) +# o.update_from_ccxt_object(ccxt_order) +# assert o.filled == 20.0 +# assert o.remaining == 0.0 +# assert not o.ft_is_open +# assert o.order_filled_date is not None +# ccxt_order.update({'id': 'somethingelse'}) +# with pytest.raises(DependencyException, match=r"Order-id's don't match"): +# o.update_from_ccxt_object(ccxt_order) +# message = "aaaa is not a valid response object." +# assert not log_has(message, caplog) +# Order.update_orders([o], 'aaaa') +# assert log_has(message, caplog) +# # Call regular update - shouldn't fail. +# Order.update_orders([o], {'id': '1234'}) From 691a042e2946fb38b60f4859bbb1fb826efedf80 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 26 Jun 2021 20:36:19 -0600 Subject: [PATCH 08/46] Set leverage and borowed to computed properties --- freqtrade/persistence/models.py | 92 +++++++++++++++++++++----------- tests/conftest.py | 34 ++++++++++++ tests/test_persistence.py | 21 +++++++- tests/test_persistence_margin.py | 32 ++++++++--- 4 files changed, 138 insertions(+), 41 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 811b7d1f8..508bb41a6 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -133,7 +133,7 @@ class Order(_DECL_BASE): order_update_date = Column(DateTime, nullable=True) leverage = Column(Float, nullable=True, default=1.0) - is_short = Column(Boolean, nullable=False, default=False) + is_short = Column(Boolean, nullable=True, default=False) def __repr__(self): return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' @@ -264,40 +264,42 @@ class LocalTrade(): timeframe: Optional[int] = None # Margin trading properties - leverage: Optional[float] = 1.0 - borrowed: float = 0.0 borrowed_currency: str = None collateral_currency: str = None interest_rate: float = 0.0 liquidation_price: float = None - is_short: bool = False + __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 + def is_short(self, val): + self.__is_short = val + + @leverage.setter + def leverage(self, lev): + self.__leverage = lev + self.__borrowed = self.amount * (lev-1) + self.amount = self.amount * lev + + @borrowed.setter + def borrowed(self, bor): + self.__leverage = self.amount / (self.amount - self.borrowed) + self.__borrowed = bor # End of margin trading properties - def __init__(self, **kwargs): - lev = kwargs.get('leverage') - bor = kwargs.get('borrowed') - amount = kwargs.get('amount') - if lev and bor: - # TODO: should I raise an error? - raise OperationalException('Cannot pass both borrowed and leverage to Trade') - elif lev: - self.amount = amount * lev - self.borrowed = amount * (lev-1) - elif bor: - self.lev = (bor + amount)/amount - - for key in kwargs: - setattr(self, key, kwargs[key]) - if not self.is_short: - self.is_short = False - self.recalc_open_trade_value() - - def __repr__(self): - open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed' - - return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' - f'open_rate={self.open_rate:.8f}, open_since={open_since})') - @property def open_date_utc(self): return self.open_date.replace(tzinfo=timezone.utc) @@ -306,6 +308,20 @@ class LocalTrade(): def close_date_utc(self): return self.close_date.replace(tzinfo=timezone.utc) + def __init__(self, **kwargs): + if kwargs.get('leverage') and kwargs.get('borrowed'): + # TODO-mg: should I raise an error? + raise OperationalException('Cannot pass both borrowed and leverage to Trade') + for key in kwargs: + setattr(self, key, kwargs[key]) + self.recalc_open_trade_value() + + def __repr__(self): + open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed' + + return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' + f'open_rate={self.open_rate:.8f}, open_since={open_since})') + def to_json(self) -> Dict[str, Any]: return { 'trade_id': self.id, @@ -448,7 +464,7 @@ class LocalTrade(): Determines if the trade is an opening (long buy or short sell) trade :param side (string): the side (buy/sell) that order happens on """ - is_short = self.is_short + is_short = self.is_short or False return (side == 'buy' and not is_short) or (side == 'sell' and is_short) def is_closing_trade(self, side) -> bool: @@ -456,7 +472,7 @@ class LocalTrade(): Determines if the trade is an closing (long sell or short buy) trade :param side (string): the side (buy/sell) that order happens on """ - is_short = self.is_short + is_short = self.is_short or False return (side == 'sell' and not is_short) or (side == 'buy' and is_short) def update(self, order: Dict) -> None: @@ -466,9 +482,14 @@ class LocalTrade(): :return: None """ order_type = order['type'] + + # if ('leverage' in order and 'borrowed' in order): + # raise OperationalException('Cannot update a trade with both borrowed and leverage') + # 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'] + # Ignore open and cancelled orders if order['status'] == 'open' or safe_value_fallback(order, 'average', 'price') is None: return @@ -477,8 +498,17 @@ class LocalTrade(): if order_type in ('market', 'limit') and self.is_opening_trade(order['side']): # Update open rate and actual amount + + # self.is_short = safe_value_fallback(order, 'is_short', default_value=False) + # self.borrowed = safe_value_fallback(order, 'is_short', default_value=False) + self.open_rate = float(safe_value_fallback(order, 'average', 'price')) self.amount = float(safe_value_fallback(order, 'filled', 'amount')) + if 'borrowed' in order: + self.borrowed = order['borrowed'] + elif 'leverage' in order: + self.leverage = order['leverage'] + self.recalc_open_trade_value() if self.is_open: payment = "SELL" if self.is_short else "BUY" diff --git a/tests/conftest.py b/tests/conftest.py index a78dd2bc2..362fb8b33 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2132,6 +2132,40 @@ def limit_exit_short_order(limit_exit_short_order_open): order['status'] = 'closed' return order +@pytest.fixture(scope='function') +def market_short_order(): + return { + 'id': 'mocked_market_buy', + 'type': 'market', + 'side': 'buy', + 'symbol': 'mocked', + 'datetime': arrow.utcnow().isoformat(), + 'price': 0.00004173, + 'amount': 91.99181073, + 'filled': 91.99181073, + 'remaining': 0.0, + 'status': 'closed', + 'is_short': True, + 'leverage': 3 + } + + +@pytest.fixture +def market_exit_short_order(): + return { + 'id': 'mocked_limit_sell', + 'type': 'market', + 'side': 'sell', + 'symbol': 'mocked', + 'datetime': arrow.utcnow().isoformat(), + 'price': 0.00004099, + 'amount': 91.99181073, + 'filled': 91.99181073, + 'remaining': 0.0, + 'status': 'closed' + } + + @pytest.fixture def interest_rate(): diff --git a/tests/test_persistence.py b/tests/test_persistence.py index f4494e967..829e3f6e7 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -659,12 +659,13 @@ def test_migrate_new(mocker, default_conf, fee, caplog): order_date DATETIME, order_filled_date DATETIME, order_update_date DATETIME, + leverage FLOAT, PRIMARY KEY (id), CONSTRAINT _order_pair_order_id UNIQUE (ft_pair, order_id), FOREIGN KEY(ft_trade_id) REFERENCES trades (id) ) """)) - + # TODO-mg @xmatthias: Had to add field leverage to this table, check that this is correct connection.execute(text(""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, remaining, cost, order_date, @@ -912,6 +913,14 @@ def test_to_json(default_conf, fee): 'strategy': None, 'timeframe': None, 'exchange': 'binance', + + 'leverage': None, + 'borrowed': None, + 'borrowed_currency': None, + 'collateral_currency': None, + 'interest_rate': None, + 'liquidation_price': None, + 'is_short': None, } # Simulate dry_run entries @@ -977,6 +986,14 @@ def test_to_json(default_conf, fee): 'strategy': None, 'timeframe': None, 'exchange': 'binance', + + 'leverage': None, + 'borrowed': None, + 'borrowed_currency': None, + 'collateral_currency': None, + 'interest_rate': None, + 'liquidation_price': None, + 'is_short': None, } @@ -1315,7 +1332,7 @@ def test_Trade_object_idem(): 'get_overall_performance', 'get_total_closed_profit', 'total_open_trades_stakes', - 'get_sold_trades_without_assigned_fees', + 'get_closed_trades_without_assigned_fees', 'get_open_trades_without_assigned_fees', 'get_open_order_trades', 'get_trades', diff --git a/tests/test_persistence_margin.py b/tests/test_persistence_margin.py index d31bde590..bbca52e50 100644 --- a/tests/test_persistence_margin.py +++ b/tests/test_persistence_margin.py @@ -58,19 +58,22 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, int fee_open=fee.return_value, fee_close=fee.return_value, interest_rate=interest_rate.return_value, - borrowed=90.99181073, - exchange='binance', - is_short=True + # borrowed=90.99181073, + exchange='binance' ) #assert trade.open_order_id is None assert trade.close_profit is None assert trade.close_date is None + assert trade.borrowed is None + assert trade.is_short is None #trade.open_order_id = 'something' trade.update(limit_short_order) #assert trade.open_order_id is None assert trade.open_rate == 0.00001173 assert trade.close_profit is None assert trade.close_date is None + assert trade.borrowed == 90.99181073 + assert trade.is_short is True assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, " r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001173, open_since=.*\).", caplog) @@ -89,7 +92,18 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, int # @pytest.mark.usefixtures("init_persistence") -# def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): +# def test_update_market_order( +# market_buy_order, +# market_sell_order, +# fee, +# interest_rate, +# ten_minutes_ago, +# caplog +# ): +# """Test Kraken and leverage arguments as well as update market order + + +# """ # trade = Trade( # id=1, # pair='ETH/BTC', @@ -99,11 +113,15 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, int # is_open=True, # fee_open=fee.return_value, # fee_close=fee.return_value, -# open_date=arrow.utcnow().datetime, -# exchange='binance', +# open_date=ten_minutes_ago, +# exchange='kraken', +# interest_rate=interest_rate.return_value # ) # trade.open_order_id = 'something' # trade.update(market_buy_order) +# assert trade.leverage is 3 +# assert trade.is_short is true +# assert trade.leverage is 3 # assert trade.open_order_id is None # assert trade.open_rate == 0.00004099 # assert trade.close_profit is None @@ -122,8 +140,6 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, int # assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " # r"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=.*\).", # caplog) -# # TODO-mg: market short -# # TODO-mg: market leveraged long # @pytest.mark.usefixtures("init_persistence") From c68a0f05d85841f107501899627929bdc6829483 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 26 Jun 2021 21:34:58 -0600 Subject: [PATCH 09/46] Added types to setters --- freqtrade/persistence/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 508bb41a6..b56876c9d 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -285,17 +285,17 @@ class LocalTrade(): return self.__is_short or False @is_short.setter - def is_short(self, val): + def is_short(self, val: bool): self.__is_short = val @leverage.setter - def leverage(self, lev): + def leverage(self, lev: float): self.__leverage = lev self.__borrowed = self.amount * (lev-1) self.amount = self.amount * lev @borrowed.setter - def borrowed(self, bor): + def borrowed(self, bor: float): self.__leverage = self.amount / (self.amount - self.borrowed) self.__borrowed = bor # End of margin trading properties From 6f6deae376b44779a121a93fd6193f2e26661704 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 27 Jun 2021 00:19:58 -0600 Subject: [PATCH 10/46] added exception checks to LocalTrade.leverage and LocalTrade.borrowed --- docs/leverage.md | 10 ++++++++ freqtrade/persistence/models.py | 44 +++++++++++++++----------------- tests/conftest.py | 6 +++-- tests/test_persistence_margin.py | 22 +++++++++------- 4 files changed, 48 insertions(+), 34 deletions(-) create mode 100644 docs/leverage.md diff --git a/docs/leverage.md b/docs/leverage.md new file mode 100644 index 000000000..658146c6f --- /dev/null +++ b/docs/leverage.md @@ -0,0 +1,10 @@ +An instance of a `Trade`/`LocalTrade` object is given either a value for `leverage` or a value for `borrowed`, but not both, on instantiation/update with a short/long. + +- If given a value for `leverage`, then the `amount` value of the `Trade`/`Local` object is multiplied by the `leverage` value to obtain the new value for `amount`. The borrowed value is also calculated from the `amount` and `leverage` value +- If given a value for `borrowed`, then the `leverage` value is calculated from `borrowed` and `amount` + +For shorts, the currency which pays the interest fee for the `borrowed` currency is purchased at the same time of the closing trade (This means that the amount purchased in short closing trades is greater than the amount sold in short opening trades). + +For longs, the currency which pays the interest fee for the `borrowed` will already be owned by the user and does not need to be purchased + +The interest fee is paid following the closing trade, or simultaneously depending on the exchange diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b56876c9d..dd81faa17 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -268,9 +268,9 @@ class LocalTrade(): collateral_currency: str = None interest_rate: float = 0.0 liquidation_price: float = None + is_short: bool = False __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 + __borrowed: float = 0.0 # * You probably want to use self.borrowed instead V @property def leverage(self) -> float: @@ -280,24 +280,22 @@ class LocalTrade(): 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 - def is_short(self, val: bool): - self.__is_short = val - @leverage.setter def leverage(self, lev: float): + if self.is_short is None or self.amount is None: + raise OperationalException( + 'LocalTrade.amount and LocalTrade.is_short must be assigned before LocalTrade.leverage') self.__leverage = lev self.__borrowed = self.amount * (lev-1) self.amount = self.amount * lev @borrowed.setter def borrowed(self, bor: float): - self.__leverage = self.amount / (self.amount - self.borrowed) + if not self.amount: + raise OperationalException( + 'LocalTrade.amount must be assigned before LocalTrade.borrowed') self.__borrowed = bor + self.__leverage = self.amount / (self.amount - self.borrowed) # End of margin trading properties @property @@ -314,6 +312,8 @@ class LocalTrade(): raise OperationalException('Cannot pass both borrowed and leverage to Trade') for key in kwargs: setattr(self, key, kwargs[key]) + if not self.is_short: + self.is_short = False self.recalc_open_trade_value() def __repr__(self): @@ -464,16 +464,14 @@ class LocalTrade(): Determines if the trade is an opening (long buy or short sell) trade :param side (string): the side (buy/sell) that order happens on """ - is_short = self.is_short or False - return (side == 'buy' and not is_short) or (side == 'sell' and is_short) + return (side == 'buy' and not self.is_short) or (side == 'sell' and self.is_short) 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 """ - is_short = self.is_short or False - return (side == 'sell' and not is_short) or (side == 'buy' and is_short) + return (side == 'sell' and not self.is_short) or (side == 'buy' and self.is_short) def update(self, order: Dict) -> None: """ @@ -483,11 +481,13 @@ class LocalTrade(): """ order_type = order['type'] - # if ('leverage' in order and 'borrowed' in order): - # raise OperationalException('Cannot update a trade with both borrowed and leverage') + if ('leverage' in order and 'borrowed' in order): + raise OperationalException( + 'Pass only one of Leverage or Borrowed to the order in update trade') - # TODO: I don't like this, but it might be the only way if 'is_short' in order and order['side'] == 'sell': + # Only set's is_short on opening trades, ignores non-shorts + # TODO-mg: I don't like this, but it might be the only way self.is_short = order['is_short'] # Ignore open and cancelled orders @@ -499,9 +499,6 @@ class LocalTrade(): if order_type in ('market', 'limit') and self.is_opening_trade(order['side']): # Update open rate and actual amount - # self.is_short = safe_value_fallback(order, 'is_short', default_value=False) - # self.borrowed = safe_value_fallback(order, 'is_short', default_value=False) - self.open_rate = float(safe_value_fallback(order, 'average', 'price')) self.amount = float(safe_value_fallback(order, 'filled', 'amount')) if 'borrowed' in order: @@ -654,7 +651,8 @@ class LocalTrade(): if self.is_short: amount = Decimal(self.amount) + interest else: - amount = Decimal(self.amount) - interest + # The interest does not need to be purchased on longs because the user already owns that currency in your wallet + amount = Decimal(self.amount) close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore fees = close_trade * Decimal(fee or self.fee_close) @@ -662,7 +660,7 @@ class LocalTrade(): if (self.is_short): return float(close_trade + fees) else: - return float(close_trade - fees) + return float(close_trade - fees - interest) def calc_profit(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float: diff --git a/tests/conftest.py b/tests/conftest.py index 362fb8b33..3d62a33e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2132,6 +2132,7 @@ def limit_exit_short_order(limit_exit_short_order_open): order['status'] = 'closed' return order + @pytest.fixture(scope='function') def market_short_order(): return { @@ -2162,11 +2163,12 @@ def market_exit_short_order(): 'amount': 91.99181073, 'filled': 91.99181073, 'remaining': 0.0, - 'status': 'closed' + 'status': 'closed', + 'leverage': 3, + 'interest_rate': 0.0005 } - @pytest.fixture def interest_rate(): return MagicMock(return_value=0.0005) diff --git a/tests/test_persistence_margin.py b/tests/test_persistence_margin.py index bbca52e50..94deb4a36 100644 --- a/tests/test_persistence_margin.py +++ b/tests/test_persistence_margin.py @@ -101,8 +101,14 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, int # caplog # ): # """Test Kraken and leverage arguments as well as update market order - - +# fee: 0.25% +# interest_rate: 0.05% per 4 hrs +# open_rate: 0.00004173 +# close_rate: 0.00004099 +# amount: 91.99181073 * leverage(3) = 275.97543219 +# borrowed: 183.98362146 +# time: 10 minutes(rounds to min of 4hrs) +# interest # """ # trade = Trade( # id=1, @@ -114,27 +120,25 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, int # fee_open=fee.return_value, # fee_close=fee.return_value, # open_date=ten_minutes_ago, -# exchange='kraken', -# interest_rate=interest_rate.return_value +# exchange='kraken' # ) # trade.open_order_id = 'something' # trade.update(market_buy_order) # assert trade.leverage is 3 -# assert trade.is_short is true -# assert trade.leverage is 3 +# assert trade.is_short is True # assert trade.open_order_id is None -# assert trade.open_rate == 0.00004099 +# assert trade.open_rate == 0.00004173 # assert trade.close_profit is None # assert trade.close_date is None # assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, " -# r"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=.*\).", +# r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004173, open_since=.*\).", # caplog) # caplog.clear() # trade.is_open = True # trade.open_order_id = 'something' # trade.update(market_sell_order) # assert trade.open_order_id is None -# assert trade.close_rate == 0.00004173 +# assert trade.close_rate == 0.00004099 # assert trade.close_profit == 0.01297561 # assert trade.close_date is not None # assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " From da81be90509cc58755db0a7f0a7f34da12252849 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 27 Jun 2021 03:38:56 -0600 Subject: [PATCH 11/46] About 15 margin tests pass --- docs/leverage.md | 2 +- freqtrade/persistence/migrations.py | 2 +- freqtrade/persistence/models.py | 184 ++++--- tests/conftest.py | 24 +- tests/rpc/test_rpc.py | 5 +- tests/test_persistence.py | 79 +++ tests/test_persistence_margin.py | 616 --------------------- tests/test_persistence_short.py | 803 ++++++++++++++++++++++++++++ 8 files changed, 1011 insertions(+), 704 deletions(-) delete mode 100644 tests/test_persistence_margin.py create mode 100644 tests/test_persistence_short.py diff --git a/docs/leverage.md b/docs/leverage.md index 658146c6f..eee1d00bb 100644 --- a/docs/leverage.md +++ b/docs/leverage.md @@ -1,7 +1,7 @@ An instance of a `Trade`/`LocalTrade` object is given either a value for `leverage` or a value for `borrowed`, but not both, on instantiation/update with a short/long. - If given a value for `leverage`, then the `amount` value of the `Trade`/`Local` object is multiplied by the `leverage` value to obtain the new value for `amount`. The borrowed value is also calculated from the `amount` and `leverage` value -- If given a value for `borrowed`, then the `leverage` value is calculated from `borrowed` and `amount` +- If given a value for `borrowed`, then the `leverage` value is left as None For shorts, the currency which pays the interest fee for the `borrowed` currency is purchased at the same time of the closing trade (This means that the amount purchased in short closing trades is greater than the amount sold in short opening trades). diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index c4e6368c5..ef4a5623b 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -48,7 +48,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') - leverage = get_column_def(cols, 'leverage', '0.0') + leverage = get_column_def(cols, 'leverage', 'null') borrowed = get_column_def(cols, 'borrowed', '0.0') borrowed_currency = get_column_def(cols, 'borrowed_currency', 'null') collateral_currency = get_column_def(cols, 'collateral_currency', 'null') diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index dd81faa17..29e2f59e3 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -132,7 +132,7 @@ class Order(_DECL_BASE): order_filled_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True) - leverage = Column(Float, nullable=True, default=1.0) + leverage = Column(Float, nullable=True, default=None) is_short = Column(Boolean, nullable=True, default=False) def __repr__(self): @@ -237,7 +237,7 @@ class LocalTrade(): close_profit: Optional[float] = None close_profit_abs: Optional[float] = None stake_amount: float = 0.0 - amount: float = 0.0 + _amount: float = 0.0 amount_requested: Optional[float] = None open_date: datetime close_date: Optional[datetime] = None @@ -269,33 +269,52 @@ class LocalTrade(): interest_rate: float = 0.0 liquidation_price: float = None is_short: bool = False - __leverage: float = 1.0 # * You probably want to use self.leverage instead | - __borrowed: float = 0.0 # * You probably want to use self.borrowed instead V + borrowed: float = 0.0 + _leverage: float = None # * You probably want to use LocalTrade.leverage instead + + # @property + # def base_currency(self) -> str: + # if not self.pair: + # raise OperationalException('LocalTrade.pair must be assigned') + # return self.pair.split("/")[1] + + @property + def amount(self) -> float: + if self.leverage is not None: + return self._amount * self.leverage + else: + return self._amount + + @amount.setter + def amount(self, value): + self._amount = value @property def leverage(self) -> float: - return self.__leverage or 1.0 - - @property - def borrowed(self) -> float: - return self.__borrowed or 0.0 + return self._leverage @leverage.setter - def leverage(self, lev: float): + def leverage(self, value): + # def set_leverage(self, lev: float, is_short: Optional[bool], amount: Optional[float]): + # TODO: Should this be @leverage.setter, or should it take arguments is_short and amount + # if is_short is None: + # is_short = self.is_short + # if amount is None: + # amount = self.amount if self.is_short is None or self.amount is None: raise OperationalException( - 'LocalTrade.amount and LocalTrade.is_short must be assigned before LocalTrade.leverage') - self.__leverage = lev - self.__borrowed = self.amount * (lev-1) - self.amount = self.amount * lev + 'LocalTrade.amount and LocalTrade.is_short must be assigned before assigning leverage') + + self._leverage = value + if self.is_short: + # If shorting the full amount must be borrowed + self.borrowed = self.amount * value + else: + # If not shorting, then the trader already owns a bit + self.borrowed = self.amount * (value-1) + # TODO: Maybe amount should be a computed property, so we don't have to modify it + self.amount = self.amount * value - @borrowed.setter - def borrowed(self, bor: float): - if not self.amount: - raise OperationalException( - 'LocalTrade.amount must be assigned before LocalTrade.borrowed') - self.__borrowed = bor - self.__leverage = self.amount / (self.amount - self.borrowed) # End of margin trading properties @property @@ -414,7 +433,10 @@ class LocalTrade(): 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) + if self.is_short: + self.stop_loss_pct = abs(stoploss) + else: + self.stop_loss_pct = -1 * abs(stoploss) self.stoploss_last_update = datetime.utcnow() def adjust_stop_loss(self, current_price: float, stoploss: float, @@ -430,17 +452,24 @@ class LocalTrade(): # Don't modify if called with initial and nothing to do return - new_loss = float(current_price * (1 - abs(stoploss))) - # TODO: Could maybe move this if into the new stoploss if branch - 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) + if self.is_short: + new_loss = float(current_price * (1 + abs(stoploss))) + 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) + else: + new_loss = float(current_price * (1 - abs(stoploss))) + if self.liquidation_price: # If trading on margin, don't set the stoploss below the liquidation price + new_loss = max(self.liquidation_price, new_loss) # no stop loss assigned yet if not self.stop_loss: logger.debug(f"{self.pair} - Assigning new stoploss...") self._set_new_stoploss(new_loss, stoploss) self.initial_stop_loss = new_loss - self.initial_stop_loss_pct = -1 * abs(stoploss) + if self.is_short: + self.initial_stop_loss_pct = abs(stoploss) + else: + self.initial_stop_loss_pct = -1 * abs(stoploss) # evaluate if the stop loss needs to be updated else: @@ -501,6 +530,7 @@ class LocalTrade(): self.open_rate = float(safe_value_fallback(order, 'average', 'price')) self.amount = float(safe_value_fallback(order, 'filled', 'amount')) + if 'borrowed' in order: self.borrowed = order['borrowed'] elif 'leverage' in order: @@ -514,6 +544,7 @@ class LocalTrade(): elif order_type in ('market', 'limit') and self.is_closing_trade(order['side']): if self.is_open: payment = "BUY" if self.is_short else "SELL" + # TODO: On Shorts technically your buying a little bit more than the amount because it's the ammount plus the interest logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.') self.close(safe_value_fallback(order, 'average', 'price')) # TODO: Double check this elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): @@ -596,60 +627,68 @@ class LocalTrade(): """ self.open_trade_value = self._calc_open_trade_value() - def calculate_interest(self) -> Decimal: + 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 + """ # 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) + zero = Decimal(0.0) + if not (self.borrowed): + return zero - 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") + open_date = self.open_date.replace(tzinfo=None) + now = datetime.utcnow() + # sec_per_day = Decimal(86400) + sec_per_hour = Decimal(3600) + total_seconds = Decimal((now - open_date).total_seconds()) + #days = total_seconds/sec_per_day or zero + hours = total_seconds/sec_per_hour or zero - rate = Decimal(self.interest_rate) + rate = Decimal(interest_rate or self.interest_rate) borrowed = Decimal(self.borrowed) - twenty4 = Decimal(24.0) one = Decimal(1.0) + twenty_four = Decimal(24.0) + four = Decimal(4.0) if self.exchange == 'binance': # Rate is per day but accrued hourly or something # binance: https://www.binance.com/en-AU/support/faq/360030157812 - return borrowed * (rate/twenty4) * max(hours, one) # TODO-mg: Is hours rounded? + return borrowed * rate * max(hours, one)/twenty_four # TODO-mg: Is hours rounded? 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) + roll_over_fee = borrowed * rate * max(0, (hours-four)/four) return opening_fee + roll_over_fee elif self.exchange == 'binance_usdm_futures': # ! TODO-mg: This is incorrect, I didn't look it up - return borrowed * (rate/twenty4) * max(hours, one) + return borrowed * (rate/twenty_four) * max(hours, one) elif self.exchange == 'binance_coinm_futures': # ! TODO-mg: This is incorrect, I didn't look it up - return borrowed * (rate/twenty4) * max(hours, one) + return borrowed * (rate/twenty_four) * max(hours, one) else: # TODO-mg: make sure this breaks and can't be squelched raise OperationalException("Leverage not available on this exchange") def calc_close_trade_value(self, rate: Optional[float] = None, - fee: Optional[float] = None) -> float: + fee: Optional[float] = None, + interest_rate: Optional[float] = None) -> float: """ Calculate the close_rate including fee :param fee: fee to use on the close rate (optional). If rate is not set self.fee will be used :param rate: rate to compare with (optional). If rate is not set self.close_rate will be used + :param interest_rate: interest_charge for borrowing this coin (optional). + If interest_rate is not set self.interest_rate will be used :return: Price in BTC of the open trade """ if rate is None and not self.close_rate: return 0.0 - interest = self.calculate_interest() + interest = self.calculate_interest(interest_rate) if self.is_short: - amount = Decimal(self.amount) + interest + amount = Decimal(self.amount) + Decimal(interest) else: # The interest does not need to be purchased on longs because the user already owns that currency in your wallet amount = Decimal(self.amount) @@ -663,18 +702,22 @@ class LocalTrade(): return float(close_trade - fees - interest) def calc_profit(self, rate: Optional[float] = None, - fee: Optional[float] = None) -> float: + fee: Optional[float] = None, + interest_rate: Optional[float] = None) -> float: """ Calculate the absolute profit in stake currency between Close and Open trade :param fee: fee to use on the close rate (optional). If fee is not set self.fee will be used :param rate: close rate to compare with (optional). If rate is not set self.close_rate will be used + :param interest_rate: interest_charge for borrowing this coin (optional). + If interest_rate is not set self.interest_rate will be used :return: profit in stake currency as float """ close_trade_value = self.calc_close_trade_value( rate=(rate or self.close_rate), - fee=(fee or self.fee_close) + fee=(fee or self.fee_close), + interest_rate=(interest_rate or self.interest_rate) ) if self.is_short: @@ -684,17 +727,21 @@ class LocalTrade(): return float(f"{profit:.8f}") def calc_profit_ratio(self, rate: Optional[float] = None, - fee: Optional[float] = None) -> float: + fee: Optional[float] = None, + interest_rate: Optional[float] = None) -> float: """ Calculates the profit as ratio (including fee). :param rate: rate to compare with (optional). If rate is not set self.close_rate will be used :param fee: fee to use on the close rate (optional). + :param interest_rate: interest_charge for borrowing this coin (optional). + If interest_rate is not set self.interest_rate will be used :return: profit ratio as float """ close_trade_value = self.calc_close_trade_value( rate=(rate or self.close_rate), - fee=(fee or self.fee_close) + fee=(fee or self.fee_close), + interest_rate=(interest_rate or self.interest_rate) ) if self.is_short: if close_trade_value == 0.0: @@ -724,7 +771,7 @@ class LocalTrade(): else: return None - @ staticmethod + @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, open_date: datetime = None, close_date: datetime = None, ) -> List['LocalTrade']: @@ -758,27 +805,27 @@ class LocalTrade(): return sel_trades - @ staticmethod + @staticmethod def close_bt_trade(trade): LocalTrade.trades_open.remove(trade) LocalTrade.trades.append(trade) LocalTrade.total_profit += trade.close_profit_abs - @ staticmethod + @staticmethod def add_bt_trade(trade): if trade.is_open: LocalTrade.trades_open.append(trade) else: LocalTrade.trades.append(trade) - @ staticmethod + @staticmethod def get_open_trades() -> List[Any]: """ Query trades from persistence layer """ return Trade.get_trades_proxy(is_open=True) - @ staticmethod + @staticmethod def stoploss_reinitialization(desired_stoploss): """ Adjust initial Stoploss to desired stoploss for all open trades. @@ -853,18 +900,19 @@ class Trade(_DECL_BASE, LocalTrade): # Lowest price reached min_rate = Column(Float, nullable=True) sell_reason = Column(String(100), nullable=True) # TODO: Change to close_reason - sell_order_status = Column(String(100), nullable=True) + sell_order_status = Column(String(100), nullable=True) # TODO: Change to close_order_status strategy = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) # Margin trading properties - leverage = Column(Float, nullable=True, default=1.0) + _leverage: float = None # * You probably want to use LocalTrade.leverage instead borrowed = Column(Float, nullable=False, default=0.0) - borrowed_currency = Column(Float, nullable=True) - collateral_currency = Column(String(25), nullable=True) interest_rate = Column(Float, nullable=False, default=0.0) liquidation_price = Column(Float, nullable=True) is_short = Column(Boolean, nullable=False, default=False) + # TODO: Bottom 2 might not be needed + borrowed_currency = Column(Float, nullable=True) + collateral_currency = Column(String(25), nullable=True) # End of margin trading properties def __init__(self, **kwargs): @@ -879,11 +927,11 @@ class Trade(_DECL_BASE, LocalTrade): Trade.query.session.delete(self) Trade.commit() - @ staticmethod + @staticmethod def commit(): Trade.query.session.commit() - @ staticmethod + @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, open_date: datetime = None, close_date: datetime = None, ) -> List['LocalTrade']: @@ -913,7 +961,7 @@ class Trade(_DECL_BASE, LocalTrade): close_date=close_date ) - @ staticmethod + @staticmethod def get_trades(trade_filter=None) -> Query: """ Helper function to query Trades using filters. @@ -933,7 +981,7 @@ class Trade(_DECL_BASE, LocalTrade): else: return Trade.query - @ staticmethod + @staticmethod def get_open_order_trades(): """ Returns all open trades @@ -941,7 +989,7 @@ class Trade(_DECL_BASE, LocalTrade): """ return Trade.get_trades(Trade.open_order_id.isnot(None)).all() - @ staticmethod + @staticmethod def get_open_trades_without_assigned_fees(): """ Returns all open trades which don't have open fees set correctly @@ -952,7 +1000,7 @@ class Trade(_DECL_BASE, LocalTrade): Trade.is_open.is_(True), ]).all() - @ staticmethod + @staticmethod def get_closed_trades_without_assigned_fees(): """ Returns all closed trades which don't have fees set correctly @@ -990,7 +1038,7 @@ class Trade(_DECL_BASE, LocalTrade): t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True)) return total_open_stake_amount or 0 - @ staticmethod + @staticmethod def get_overall_performance() -> List[Dict[str, Any]]: """ Returns List of dicts containing all Trades, including profit and trade count @@ -1053,7 +1101,7 @@ class PairLock(_DECL_BASE): return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, ' f'lock_end_time={lock_end_time})') - @ staticmethod + @staticmethod def query_pair_locks(pair: Optional[str], now: datetime) -> Query: """ Get all currently active locks for this pair diff --git a/tests/conftest.py b/tests/conftest.py index 3d62a33e2..3c071f2f3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,9 +57,9 @@ def log_has_re(line, logs): def get_args(args): return Arguments(args).get_parsed_arg() + + # Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines - - def get_mock_coro(return_value): async def mock_coro(*args, **kwargs): return return_value @@ -2075,7 +2075,7 @@ def ten_minutes_ago(): @pytest.fixture def five_hours_ago(): - return datetime.utcnow() - timedelta(hours=1, minutes=0) + return datetime.utcnow() - timedelta(hours=5, minutes=0) @pytest.fixture(scope='function') @@ -2136,9 +2136,9 @@ def limit_exit_short_order(limit_exit_short_order_open): @pytest.fixture(scope='function') def market_short_order(): return { - 'id': 'mocked_market_buy', + 'id': 'mocked_market_short', 'type': 'market', - 'side': 'buy', + 'side': 'sell', 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), 'price': 0.00004173, @@ -2147,16 +2147,16 @@ def market_short_order(): 'remaining': 0.0, 'status': 'closed', 'is_short': True, - 'leverage': 3 + 'leverage': 3.0 } @pytest.fixture def market_exit_short_order(): return { - 'id': 'mocked_limit_sell', + 'id': 'mocked_limit_exit_short', 'type': 'market', - 'side': 'sell', + 'side': 'buy', 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), 'price': 0.00004099, @@ -2164,11 +2164,5 @@ def market_exit_short_order(): 'filled': 91.99181073, 'remaining': 0.0, 'status': 'closed', - 'leverage': 3, - 'interest_rate': 0.0005 + 'leverage': 3.0 } - - -@pytest.fixture -def interest_rate(): - return MagicMock(return_value=0.0005) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 50c1a0b31..e324626c3 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -108,7 +108,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'open_order': None, 'exchange': 'binance', - 'leverage': 1.0, + 'leverage': None, 'borrowed': 0.0, 'borrowed_currency': None, 'collateral_currency': None, @@ -182,14 +182,13 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'open_order': None, 'exchange': 'binance', - 'leverage': 1.0, + 'leverage': None, 'borrowed': 0.0, 'borrowed_currency': None, 'collateral_currency': None, 'interest_rate': 0.0, 'liquidation_price': None, 'is_short': False, - } diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 829e3f6e7..40542f943 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -63,6 +63,48 @@ def test_init_dryrun_db(default_conf, tmpdir): assert Path(filename).is_file() +@pytest.mark.usefixtures("init_persistence") +def test_is_opening_closing_trade(fee): + trade = Trade( + id=2, + pair='ETH/BTC', + stake_amount=0.001, + open_rate=0.01, + amount=5, + is_open=True, + open_date=arrow.utcnow().datetime, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + is_short=False, + leverage=2.0 + ) + assert trade.is_opening_trade('buy') == True + assert trade.is_opening_trade('sell') == False + assert trade.is_closing_trade('buy') == False + assert trade.is_closing_trade('sell') == True + + trade = Trade( + id=2, + pair='ETH/BTC', + stake_amount=0.001, + open_rate=0.01, + amount=5, + is_open=True, + open_date=arrow.utcnow().datetime, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + is_short=True, + leverage=2.0 + ) + + assert trade.is_opening_trade('buy') == False + assert trade.is_opening_trade('sell') == True + assert trade.is_closing_trade('buy') == True + assert trade.is_closing_trade('sell') == False + + @pytest.mark.usefixtures("init_persistence") def test_update_with_binance(limit_buy_order, limit_sell_order, fee, caplog): """ @@ -196,6 +238,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): @pytest.mark.usefixtures("init_persistence") def test_trade_close(limit_buy_order, limit_sell_order, fee): + # TODO: limit_buy_order and limit_sell_order aren't used, remove them probably trade = Trade( pair='ETH/BTC', stake_amount=0.001, @@ -1126,6 +1169,42 @@ def test_fee_updated(fee): assert not trade.fee_updated('asfd') +@pytest.mark.usefixtures("init_persistence") +def test_update_leverage(fee, ten_minutes_ago): + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + open_rate=0.00001099, + open_date=ten_minutes_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + is_short=True, + interest_rate=0.0005 + ) + trade.leverage = 3.0 + assert trade.borrowed == 15.0 + assert trade.amount == 15.0 + + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + open_rate=0.00001099, + open_date=ten_minutes_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + is_short=False, + interest_rate=0.0005 + ) + + trade.leverage = 5.0 + assert trade.borrowed == 20.0 + assert trade.amount == 25.0 + + @pytest.mark.usefixtures("init_persistence") @pytest.mark.parametrize('use_db', [True, False]) def test_total_open_trades_stakes(fee, use_db): diff --git a/tests/test_persistence_margin.py b/tests/test_persistence_margin.py deleted file mode 100644 index 94deb4a36..000000000 --- a/tests/test_persistence_margin.py +++ /dev/null @@ -1,616 +0,0 @@ -import logging -from datetime import datetime, timedelta, timezone -from pathlib import Path -from types import FunctionType -from unittest.mock import MagicMock -import arrow -import pytest -from sqlalchemy import create_engine, inspect, text -from freqtrade import constants -from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db -from tests.conftest import create_mock_trades, log_has, log_has_re - -# * Margin tests - - -@pytest.mark.usefixtures("init_persistence") -def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, interest_rate, ten_minutes_ago, caplog): - """ - On this test we will short and buy back(exit short) a crypto currency at 1x leverage - #*The actual program uses more precise numbers - Short - - Sell: 90.99181073 Crypto at 0.00001173 BTC - - Selling fee: 0.25% - - Total value of sell trade: 0.001064666 BTC - ((90.99181073*0.00001173) - ((90.99181073*0.00001173)*0.0025)) - Exit Short - - Buy: 90.99181073 Crypto at 0.00001099 BTC - - Buying fee: 0.25% - - Interest fee: 0.05% - - Total interest - (90.99181073 * 0.0005)/24 = 0.00189566272 - - Total cost of buy trade: 0.00100252088 - (90.99181073 + 0.00189566272) * 0.00001099 = 0.00100002083 :(borrowed + interest * cost) - + ((90.99181073 + 0.00189566272)*0.00001099)*0.0025 = 0.00000250005 - = 0.00100252088 - - Profit/Loss: +0.00006214512 BTC - Sell:0.001064666 - Buy:0.00100252088 - Profit/Loss percentage: 0.06198885353 - (0.001064666/0.00100252088)-1 = 0.06198885353 - #* ~0.061988453889463014104555743 With more precise numbers used - :param limit_short_order: - :param limit_exit_short_order: - :param fee - :param interest_rate - :param caplog - :return: - """ - trade = Trade( - id=2, - pair='ETH/BTC', - stake_amount=0.001, - open_rate=0.01, - amount=5, - is_open=True, - open_date=ten_minutes_ago, - fee_open=fee.return_value, - fee_close=fee.return_value, - interest_rate=interest_rate.return_value, - # borrowed=90.99181073, - exchange='binance' - ) - #assert trade.open_order_id is None - assert trade.close_profit is None - assert trade.close_date is None - assert trade.borrowed is None - assert trade.is_short is None - #trade.open_order_id = 'something' - trade.update(limit_short_order) - #assert trade.open_order_id is None - assert trade.open_rate == 0.00001173 - assert trade.close_profit is None - assert trade.close_date is None - assert trade.borrowed == 90.99181073 - assert trade.is_short is True - assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, " - r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001173, open_since=.*\).", - caplog) - caplog.clear() - #trade.open_order_id = 'something' - trade.update(limit_exit_short_order) - #assert trade.open_order_id is None - assert trade.close_rate == 0.00001099 - assert trade.close_profit == 0.06198845 - assert trade.close_date is not None - assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, " - r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001173, open_since=.*\).", - caplog) - - # TODO-mg: create a leveraged long order - - -# @pytest.mark.usefixtures("init_persistence") -# def test_update_market_order( -# market_buy_order, -# market_sell_order, -# fee, -# interest_rate, -# ten_minutes_ago, -# caplog -# ): -# """Test Kraken and leverage arguments as well as update market order -# fee: 0.25% -# interest_rate: 0.05% per 4 hrs -# open_rate: 0.00004173 -# close_rate: 0.00004099 -# amount: 91.99181073 * leverage(3) = 275.97543219 -# borrowed: 183.98362146 -# time: 10 minutes(rounds to min of 4hrs) -# interest -# """ -# trade = Trade( -# id=1, -# pair='ETH/BTC', -# stake_amount=0.001, -# amount=5, -# open_rate=0.01, -# is_open=True, -# fee_open=fee.return_value, -# fee_close=fee.return_value, -# open_date=ten_minutes_ago, -# exchange='kraken' -# ) -# trade.open_order_id = 'something' -# trade.update(market_buy_order) -# assert trade.leverage is 3 -# assert trade.is_short is True -# assert trade.open_order_id is None -# assert trade.open_rate == 0.00004173 -# assert trade.close_profit is None -# assert trade.close_date is None -# assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, " -# r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004173, open_since=.*\).", -# caplog) -# caplog.clear() -# trade.is_open = True -# trade.open_order_id = 'something' -# trade.update(market_sell_order) -# assert trade.open_order_id is None -# assert trade.close_rate == 0.00004099 -# assert trade.close_profit == 0.01297561 -# assert trade.close_date is not None -# assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " -# r"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=.*\).", -# caplog) - - -# @pytest.mark.usefixtures("init_persistence") -# def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): -# trade = Trade( -# pair='ETH/BTC', -# stake_amount=0.001, -# open_rate=0.01, -# amount=5, -# fee_open=fee.return_value, -# fee_close=fee.return_value, -# exchange='binance', -# ) -# trade.open_order_id = 'something' -# trade.update(limit_buy_order) -# assert trade._calc_open_trade_value() == 0.0010024999999225068 -# trade.update(limit_sell_order) -# assert trade.calc_close_trade_value() == 0.0010646656050132426 -# # Profit in BTC -# assert trade.calc_profit() == 0.00006217 -# # Profit in percent -# assert trade.calc_profit_ratio() == 0.06201058 - - -# @pytest.mark.usefixtures("init_persistence") -# def test_trade_close(limit_buy_order, limit_sell_order, fee): -# trade = Trade( -# pair='ETH/BTC', -# stake_amount=0.001, -# open_rate=0.01, -# amount=5, -# is_open=True, -# fee_open=fee.return_value, -# fee_close=fee.return_value, -# open_date=arrow.Arrow(2020, 2, 1, 15, 5, 1).datetime, -# exchange='binance', -# ) -# assert trade.close_profit is None -# assert trade.close_date is None -# assert trade.is_open is True -# trade.close(0.02) -# assert trade.is_open is False -# assert trade.close_profit == 0.99002494 -# assert trade.close_date is not None -# new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, -# assert trade.close_date != new_date -# # Close should NOT update close_date if the trade has been closed already -# assert trade.is_open is False -# trade.close_date = new_date -# trade.close(0.02) -# assert trade.close_date == new_date - - -# @pytest.mark.usefixtures("init_persistence") -# def test_calc_close_trade_price_exception(limit_buy_order, fee): -# trade = Trade( -# pair='ETH/BTC', -# stake_amount=0.001, -# open_rate=0.1, -# amount=5, -# fee_open=fee.return_value, -# fee_close=fee.return_value, -# exchange='binance', -# ) -# trade.open_order_id = 'something' -# trade.update(limit_buy_order) -# assert trade.calc_close_trade_value() == 0.0 - - -# @pytest.mark.usefixtures("init_persistence") -# def test_update_open_order(limit_buy_order): -# trade = Trade( -# pair='ETH/BTC', -# stake_amount=1.00, -# open_rate=0.01, -# amount=5, -# fee_open=0.1, -# fee_close=0.1, -# exchange='binance', -# ) -# assert trade.open_order_id is None -# assert trade.close_profit is None -# assert trade.close_date is None -# limit_buy_order['status'] = 'open' -# trade.update(limit_buy_order) -# assert trade.open_order_id is None -# assert trade.close_profit is None -# assert trade.close_date is None - - -# @pytest.mark.usefixtures("init_persistence") -# def test_calc_open_trade_value(limit_buy_order, fee): -# trade = Trade( -# pair='ETH/BTC', -# stake_amount=0.001, -# amount=5, -# open_rate=0.00001099, -# fee_open=fee.return_value, -# fee_close=fee.return_value, -# exchange='binance', -# ) -# trade.open_order_id = 'open_trade' -# trade.update(limit_buy_order) # Buy @ 0.00001099 -# # Get the open rate price with the standard fee rate -# assert trade._calc_open_trade_value() == 0.0010024999999225068 -# trade.fee_open = 0.003 -# # Get the open rate price with a custom fee rate -# assert trade._calc_open_trade_value() == 0.001002999999922468 - - -# @pytest.mark.usefixtures("init_persistence") -# def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee): -# trade = Trade( -# pair='ETH/BTC', -# stake_amount=0.001, -# amount=5, -# open_rate=0.00001099, -# fee_open=fee.return_value, -# fee_close=fee.return_value, -# exchange='binance', -# ) -# trade.open_order_id = 'close_trade' -# trade.update(limit_buy_order) # Buy @ 0.00001099 -# # Get the close rate price with a custom close rate and a regular fee rate -# assert trade.calc_close_trade_value(rate=0.00001234) == 0.0011200318470471794 -# # Get the close rate price with a custom close rate and a custom fee rate -# assert trade.calc_close_trade_value(rate=0.00001234, fee=0.003) == 0.0011194704275749754 -# # Test when we apply a Sell order, and ask price with a custom fee rate -# trade.update(limit_sell_order) -# assert trade.calc_close_trade_value(fee=0.005) == 0.0010619972701635854 - - -# @pytest.mark.usefixtures("init_persistence") -# def test_calc_profit(limit_buy_order, limit_sell_order, fee): -# trade = Trade( -# pair='ETH/BTC', -# stake_amount=0.001, -# amount=5, -# open_rate=0.00001099, -# fee_open=fee.return_value, -# fee_close=fee.return_value, -# exchange='binance', -# ) -# trade.open_order_id = 'something' -# trade.update(limit_buy_order) # Buy @ 0.00001099 -# # Custom closing rate and regular fee rate -# # Higher than open rate -# assert trade.calc_profit(rate=0.00001234) == 0.00011753 -# # Lower than open rate -# assert trade.calc_profit(rate=0.00000123) == -0.00089086 -# # Custom closing rate and custom fee rate -# # Higher than open rate -# assert trade.calc_profit(rate=0.00001234, fee=0.003) == 0.00011697 -# # Lower than open rate -# assert trade.calc_profit(rate=0.00000123, fee=0.003) == -0.00089092 -# # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 -# trade.update(limit_sell_order) -# assert trade.calc_profit() == 0.00006217 -# # Test with a custom fee rate on the close trade -# assert trade.calc_profit(fee=0.003) == 0.00006163 - - -# @pytest.mark.usefixtures("init_persistence") -# def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee): -# trade = Trade( -# pair='ETH/BTC', -# stake_amount=0.001, -# amount=5, -# open_rate=0.00001099, -# fee_open=fee.return_value, -# fee_close=fee.return_value, -# exchange='binance', -# ) -# trade.open_order_id = 'something' -# trade.update(limit_buy_order) # Buy @ 0.00001099 -# # Get percent of profit with a custom rate (Higher than open rate) -# assert trade.calc_profit_ratio(rate=0.00001234) == 0.11723875 -# # Get percent of profit with a custom rate (Lower than open rate) -# assert trade.calc_profit_ratio(rate=0.00000123) == -0.88863828 -# # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 -# trade.update(limit_sell_order) -# assert trade.calc_profit_ratio() == 0.06201058 -# # Test with a custom fee rate on the close trade -# assert trade.calc_profit_ratio(fee=0.003) == 0.06147824 -# trade.open_trade_value = 0.0 -# assert trade.calc_profit_ratio(fee=0.003) == 0.0 - - -# def test_adjust_stop_loss(fee): -# trade = Trade( -# pair='ETH/BTC', -# stake_amount=0.001, -# amount=5, -# fee_open=fee.return_value, -# fee_close=fee.return_value, -# exchange='binance', -# open_rate=1, -# max_rate=1, -# ) -# trade.adjust_stop_loss(trade.open_rate, 0.05, True) -# assert trade.stop_loss == 0.95 -# assert trade.stop_loss_pct == -0.05 -# assert trade.initial_stop_loss == 0.95 -# assert trade.initial_stop_loss_pct == -0.05 -# # Get percent of profit with a lower rate -# trade.adjust_stop_loss(0.96, 0.05) -# assert trade.stop_loss == 0.95 -# assert trade.stop_loss_pct == -0.05 -# assert trade.initial_stop_loss == 0.95 -# assert trade.initial_stop_loss_pct == -0.05 -# # Get percent of profit with a custom rate (Higher than open rate) -# trade.adjust_stop_loss(1.3, -0.1) -# assert round(trade.stop_loss, 8) == 1.17 -# assert trade.stop_loss_pct == -0.1 -# assert trade.initial_stop_loss == 0.95 -# assert trade.initial_stop_loss_pct == -0.05 -# # current rate lower again ... should not change -# trade.adjust_stop_loss(1.2, 0.1) -# assert round(trade.stop_loss, 8) == 1.17 -# assert trade.initial_stop_loss == 0.95 -# assert trade.initial_stop_loss_pct == -0.05 -# # current rate higher... should raise stoploss -# trade.adjust_stop_loss(1.4, 0.1) -# assert round(trade.stop_loss, 8) == 1.26 -# assert trade.initial_stop_loss == 0.95 -# assert trade.initial_stop_loss_pct == -0.05 -# # Initial is true but stop_loss set - so doesn't do anything -# trade.adjust_stop_loss(1.7, 0.1, True) -# assert round(trade.stop_loss, 8) == 1.26 -# assert trade.initial_stop_loss == 0.95 -# assert trade.initial_stop_loss_pct == -0.05 -# assert trade.stop_loss_pct == -0.1 - - -# def test_adjust_min_max_rates(fee): -# trade = Trade( -# pair='ETH/BTC', -# stake_amount=0.001, -# amount=5, -# fee_open=fee.return_value, -# fee_close=fee.return_value, -# exchange='binance', -# open_rate=1, -# ) -# trade.adjust_min_max_rates(trade.open_rate) -# assert trade.max_rate == 1 -# assert trade.min_rate == 1 -# # check min adjusted, max remained -# trade.adjust_min_max_rates(0.96) -# assert trade.max_rate == 1 -# assert trade.min_rate == 0.96 -# # check max adjusted, min remains -# trade.adjust_min_max_rates(1.05) -# assert trade.max_rate == 1.05 -# assert trade.min_rate == 0.96 -# # current rate "in the middle" - no adjustment -# trade.adjust_min_max_rates(1.03) -# assert trade.max_rate == 1.05 -# assert trade.min_rate == 0.96 - - -# @pytest.mark.usefixtures("init_persistence") -# @pytest.mark.parametrize('use_db', [True, False]) -# def test_get_open(fee, use_db): -# Trade.use_db = use_db -# Trade.reset_trades() -# create_mock_trades(fee, use_db) -# assert len(Trade.get_open_trades()) == 4 -# Trade.use_db = True - - -# def test_stoploss_reinitialization(default_conf, fee): -# init_db(default_conf['db_url']) -# trade = Trade( -# pair='ETH/BTC', -# stake_amount=0.001, -# fee_open=fee.return_value, -# open_date=arrow.utcnow().shift(hours=-2).datetime, -# amount=10, -# fee_close=fee.return_value, -# exchange='binance', -# open_rate=1, -# max_rate=1, -# ) -# trade.adjust_stop_loss(trade.open_rate, 0.05, True) -# assert trade.stop_loss == 0.95 -# assert trade.stop_loss_pct == -0.05 -# assert trade.initial_stop_loss == 0.95 -# assert trade.initial_stop_loss_pct == -0.05 -# Trade.query.session.add(trade) -# # Lower stoploss -# Trade.stoploss_reinitialization(0.06) -# trades = Trade.get_open_trades() -# assert len(trades) == 1 -# trade_adj = trades[0] -# assert trade_adj.stop_loss == 0.94 -# assert trade_adj.stop_loss_pct == -0.06 -# assert trade_adj.initial_stop_loss == 0.94 -# assert trade_adj.initial_stop_loss_pct == -0.06 -# # Raise stoploss -# Trade.stoploss_reinitialization(0.04) -# trades = Trade.get_open_trades() -# assert len(trades) == 1 -# trade_adj = trades[0] -# assert trade_adj.stop_loss == 0.96 -# assert trade_adj.stop_loss_pct == -0.04 -# assert trade_adj.initial_stop_loss == 0.96 -# assert trade_adj.initial_stop_loss_pct == -0.04 -# # Trailing stoploss (move stoplos up a bit) -# trade.adjust_stop_loss(1.02, 0.04) -# assert trade_adj.stop_loss == 0.9792 -# assert trade_adj.initial_stop_loss == 0.96 -# Trade.stoploss_reinitialization(0.04) -# trades = Trade.get_open_trades() -# assert len(trades) == 1 -# trade_adj = trades[0] -# # Stoploss should not change in this case. -# assert trade_adj.stop_loss == 0.9792 -# assert trade_adj.stop_loss_pct == -0.04 -# assert trade_adj.initial_stop_loss == 0.96 -# assert trade_adj.initial_stop_loss_pct == -0.04 - - -# def test_update_fee(fee): -# trade = Trade( -# pair='ETH/BTC', -# stake_amount=0.001, -# fee_open=fee.return_value, -# open_date=arrow.utcnow().shift(hours=-2).datetime, -# amount=10, -# fee_close=fee.return_value, -# exchange='binance', -# open_rate=1, -# max_rate=1, -# ) -# fee_cost = 0.15 -# fee_currency = 'BTC' -# fee_rate = 0.0075 -# assert trade.fee_open_currency is None -# assert not trade.fee_updated('buy') -# assert not trade.fee_updated('sell') -# trade.update_fee(fee_cost, fee_currency, fee_rate, 'buy') -# assert trade.fee_updated('buy') -# assert not trade.fee_updated('sell') -# assert trade.fee_open_currency == fee_currency -# assert trade.fee_open_cost == fee_cost -# assert trade.fee_open == fee_rate -# # Setting buy rate should "guess" close rate -# assert trade.fee_close == fee_rate -# assert trade.fee_close_currency is None -# assert trade.fee_close_cost is None -# fee_rate = 0.0076 -# trade.update_fee(fee_cost, fee_currency, fee_rate, 'sell') -# assert trade.fee_updated('buy') -# assert trade.fee_updated('sell') -# assert trade.fee_close == 0.0076 -# assert trade.fee_close_cost == fee_cost -# assert trade.fee_close == fee_rate - - -# def test_fee_updated(fee): -# trade = Trade( -# pair='ETH/BTC', -# stake_amount=0.001, -# fee_open=fee.return_value, -# open_date=arrow.utcnow().shift(hours=-2).datetime, -# amount=10, -# fee_close=fee.return_value, -# exchange='binance', -# open_rate=1, -# max_rate=1, -# ) -# assert trade.fee_open_currency is None -# assert not trade.fee_updated('buy') -# assert not trade.fee_updated('sell') -# assert not trade.fee_updated('asdf') -# trade.update_fee(0.15, 'BTC', 0.0075, 'buy') -# assert trade.fee_updated('buy') -# assert not trade.fee_updated('sell') -# assert trade.fee_open_currency is not None -# assert trade.fee_close_currency is None -# trade.update_fee(0.15, 'ABC', 0.0075, 'sell') -# assert trade.fee_updated('buy') -# assert trade.fee_updated('sell') -# assert not trade.fee_updated('asfd') - - -# @pytest.mark.usefixtures("init_persistence") -# @pytest.mark.parametrize('use_db', [True, False]) -# def test_total_open_trades_stakes(fee, use_db): -# Trade.use_db = use_db -# Trade.reset_trades() -# res = Trade.total_open_trades_stakes() -# assert res == 0 -# create_mock_trades(fee, use_db) -# res = Trade.total_open_trades_stakes() -# assert res == 0.004 -# Trade.use_db = True - - -# @pytest.mark.usefixtures("init_persistence") -# def test_get_overall_performance(fee): -# create_mock_trades(fee) -# res = Trade.get_overall_performance() -# assert len(res) == 2 -# assert 'pair' in res[0] -# assert 'profit' in res[0] -# assert 'count' in res[0] - - -# @pytest.mark.usefixtures("init_persistence") -# def test_get_best_pair(fee): -# res = Trade.get_best_pair() -# assert res is None -# create_mock_trades(fee) -# res = Trade.get_best_pair() -# assert len(res) == 2 -# assert res[0] == 'XRP/BTC' -# assert res[1] == 0.01 - - -# @pytest.mark.usefixtures("init_persistence") -# def test_update_order_from_ccxt(caplog): -# # Most basic order return (only has orderid) -# o = Order.parse_from_ccxt_object({'id': '1234'}, 'ETH/BTC', 'buy') -# assert isinstance(o, Order) -# assert o.ft_pair == 'ETH/BTC' -# assert o.ft_order_side == 'buy' -# assert o.order_id == '1234' -# assert o.ft_is_open -# ccxt_order = { -# 'id': '1234', -# 'side': 'buy', -# 'symbol': 'ETH/BTC', -# 'type': 'limit', -# 'price': 1234.5, -# 'amount': 20.0, -# 'filled': 9, -# 'remaining': 11, -# 'status': 'open', -# 'timestamp': 1599394315123 -# } -# o = Order.parse_from_ccxt_object(ccxt_order, 'ETH/BTC', 'buy') -# assert isinstance(o, Order) -# assert o.ft_pair == 'ETH/BTC' -# assert o.ft_order_side == 'buy' -# assert o.order_id == '1234' -# assert o.order_type == 'limit' -# assert o.price == 1234.5 -# assert o.filled == 9 -# assert o.remaining == 11 -# assert o.order_date is not None -# assert o.ft_is_open -# assert o.order_filled_date is None -# # Order has been closed -# ccxt_order.update({'filled': 20.0, 'remaining': 0.0, 'status': 'closed'}) -# o.update_from_ccxt_object(ccxt_order) -# assert o.filled == 20.0 -# assert o.remaining == 0.0 -# assert not o.ft_is_open -# assert o.order_filled_date is not None -# ccxt_order.update({'id': 'somethingelse'}) -# with pytest.raises(DependencyException, match=r"Order-id's don't match"): -# o.update_from_ccxt_object(ccxt_order) -# message = "aaaa is not a valid response object." -# assert not log_has(message, caplog) -# Order.update_orders([o], 'aaaa') -# assert log_has(message, caplog) -# # Call regular update - shouldn't fail. -# Order.update_orders([o], {'id': '1234'}) diff --git a/tests/test_persistence_short.py b/tests/test_persistence_short.py new file mode 100644 index 000000000..84d9329b8 --- /dev/null +++ b/tests/test_persistence_short.py @@ -0,0 +1,803 @@ +import logging +from datetime import datetime, timedelta, timezone +from pathlib import Path +from types import FunctionType +from unittest.mock import MagicMock +import arrow +import pytest +from math import isclose +from sqlalchemy import create_engine, inspect, text +from freqtrade import constants +from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db +from tests.conftest import create_mock_trades, log_has, log_has_re + + +@pytest.mark.usefixtures("init_persistence") +def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, ten_minutes_ago, caplog): + """ + 10 minute short limit trade on binance + + Short trade + fee: 0.25% base + interest_rate: 0.05% per day + open_rate: 0.00001173 base + close_rate: 0.00001099 base + amount: 90.99181073 crypto + borrowed: 90.99181073 crypto + time-periods: 10 minutes(rounds up to 1/24 time-period of 1 day) + interest: borrowed * interest_rate * time-periods + = 90.99181073 * 0.0005 * 1/24 = 0.0018956627235416667 crypto + open_value: (amount * open_rate) - (amount * open_rate * fee) + = 90.99181073 * 0.00001173 - 90.99181073 * 0.00001173 * 0.0025 + = 0.0010646656050132426 + amount_closed: amount + interest = 90.99181073 + 0.0018956627235416667 = 90.99370639272354 + close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) + = (90.99370639272354 * 0.00001099) + (90.99370639272354 * 0.00001099 * 0.0025) + = 0.0010025208853391716 + total_profit = open_value - close_value + = 0.0010646656050132426 - 0.0010025208853391716 + = 0.00006214471967407108 + total_profit_percentage = (open_value/close_value) - 1 + = (0.0010646656050132426/0.0010025208853391716)-1 + = 0.06198845388946328 + """ + trade = Trade( + id=2, + pair='ETH/BTC', + stake_amount=0.001, + open_rate=0.01, + amount=5, + is_open=True, + open_date=ten_minutes_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + # borrowed=90.99181073, + interest_rate=0.0005, + exchange='binance' + ) + #assert trade.open_order_id is None + assert trade.close_profit is None + assert trade.close_date is None + assert trade.borrowed is None + assert trade.is_short is None + #trade.open_order_id = 'something' + trade.update(limit_short_order) + #assert trade.open_order_id is None + assert trade.open_rate == 0.00001173 + assert trade.close_profit is None + assert trade.close_date is None + assert trade.borrowed == 90.99181073 + assert trade.is_short is True + assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, " + r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001173, open_since=.*\).", + caplog) + caplog.clear() + #trade.open_order_id = 'something' + trade.update(limit_exit_short_order) + #assert trade.open_order_id is None + assert trade.close_rate == 0.00001099 + assert trade.close_profit == 0.06198845 + assert trade.close_date is not None + assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, " + r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001173, open_since=.*\).", + caplog) + + +@pytest.mark.usefixtures("init_persistence") +def test_update_market_order( + market_short_order, + market_exit_short_order, + fee, + ten_minutes_ago, + caplog +): + """ + 10 minute short market trade on Kraken at 3x leverage + Short trade + fee: 0.25% base + interest_rate: 0.05% per 4 hrs + open_rate: 0.00004173 base + close_rate: 0.00004099 base + amount: 91.99181073 * leverage(3) = 275.97543219 crypto + borrowed: 275.97543219 crypto + time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) + interest: borrowed * interest_rate * time-periods + = 275.97543219 * 0.0005 * 1 = 0.137987716095 crypto + open_value: (amount * open_rate) - (amount * open_rate * fee) + = 275.97543219 * 0.00004173 - 275.97543219 * 0.00004173 * 0.0025 + = 0.011487663648325479 + amount_closed: amount + interest = 275.97543219 + 0.137987716095 = 276.113419906095 + close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) + = (276.113419906095 * 0.00004099) + (276.113419906095 * 0.00004099 * 0.0025) + = 0.01134618380465571 + total_profit = open_value - close_value + = 0.011487663648325479 - 0.01134618380465571 + = 0.00014147984366976937 + total_profit_percentage = (open_value/close_value) - 1 + = (0.011487663648325479/0.01134618380465571)-1 + = 0.012469377026284034 + """ + trade = Trade( + id=1, + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + open_rate=0.01, + is_open=True, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_date=ten_minutes_ago, + interest_rate=0.0005, + exchange='kraken' + ) + trade.open_order_id = 'something' + trade.update(market_short_order) + assert trade.leverage == 3.0 + assert trade.is_short == True + assert trade.open_order_id is None + assert trade.open_rate == 0.00004173 + assert trade.close_profit is None + assert trade.close_date is None + assert trade.interest_rate == 0.0005 + # TODO: Uncomment the next assert and make it work. + # The logger also has the exact same but there's some spacing in there + # assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " + # r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004173, open_since=.*\).", + # caplog) + caplog.clear() + trade.is_open = True + trade.open_order_id = 'something' + trade.update(market_exit_short_order) + assert trade.open_order_id is None + assert trade.close_rate == 0.00004099 + assert trade.close_profit == 0.01246938 + assert trade.close_date is not None + # TODO: The amount should maybe be the opening amount + the interest + # TODO: Uncomment the next assert and make it work. + # The logger also has the exact same but there's some spacing in there + # assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " + # r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004099, open_since=.*\).", + # caplog) + + +@pytest.mark.usefixtures("init_persistence") +def test_calc_open_close_trade_price(limit_short_order, limit_exit_short_order, five_hours_ago, fee): + """ + 5 hour short trade on Binance + Short trade + fee: 0.25% base + interest_rate: 0.05% per day + open_rate: 0.00001173 base + close_rate: 0.00001099 base + amount: 90.99181073 crypto + borrowed: 90.99181073 crypto + time-periods: 5 hours = 5/24 + interest: borrowed * interest_rate * time-periods + = 90.99181073 * 0.0005 * 5/24 = 0.009478313617708333 crypto + open_value: (amount * open_rate) - (amount * open_rate * fee) + = 90.99181073 * 0.00001173 - 90.99181073 * 0.00001173 * 0.0025 + = 0.0010646656050132426 + amount_closed: amount + interest = 90.99181073 + 0.009478313617708333 = 91.0012890436177 + close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) + = (91.0012890436177 * 0.00001099) + (91.0012890436177 * 0.00001099 * 0.0025) + = 0.001002604427005832 + total_profit = open_value - close_value + = 0.0010646656050132426 - 0.001002604427005832 + = 0.00006206117800741065 + total_profit_percentage = (open_value/close_value) - 1 + = (0.0010646656050132426/0.0010025208853391716)-1 + = 0.06189996406932852 + """ + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + open_rate=0.01, + amount=5, + open_date=five_hours_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + interest_rate=0.0005 + ) + trade.open_order_id = 'something' + trade.update(limit_short_order) + assert trade._calc_open_trade_value() == 0.0010646656050132426 + trade.update(limit_exit_short_order) + + assert isclose(trade.calc_close_trade_value(), 0.001002604427005832) + # Profit in BTC + assert isclose(trade.calc_profit(), 0.00006206) + #Profit in percent + assert isclose(trade.calc_profit_ratio(), 0.06189996) + + +@pytest.mark.usefixtures("init_persistence") +def test_trade_close(fee, five_hours_ago): + """ + Five hour short trade on Kraken at 3x leverage + Short trade + Exchange: Kraken + fee: 0.25% base + interest_rate: 0.05% per 4 hours + open_rate: 0.02 base + close_rate: 0.01 base + leverage: 3.0 + amount: 5 * 3 = 15 crypto + borrowed: 15 crypto + time-periods: 5 hours = 5/4 + + interest: borrowed * interest_rate * time-periods + = 15 * 0.0005 * 5/4 = 0.009375 crypto + open_value: (amount * open_rate) - (amount * open_rate * fee) + = (15 * 0.02) - (15 * 0.02 * 0.0025) + = 0.29925 + amount_closed: amount + interest = 15 + 0.009375 = 15.009375 + close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) + = (15.009375 * 0.01) + (15.009375 * 0.01 * 0.0025) + = 0.150468984375 + total_profit = open_value - close_value + = 0.29925 - 0.150468984375 + = 0.148781015625 + total_profit_percentage = (open_value/close_value) - 1 + = (0.29925/0.150468984375)-1 + = 0.9887819489377738 + """ + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + open_rate=0.02, + amount=5, + is_open=True, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_date=five_hours_ago, + exchange='kraken', + is_short=True, + leverage=3.0, + interest_rate=0.0005 + ) + assert trade.close_profit is None + assert trade.close_date is None + assert trade.is_open is True + trade.close(0.01) + assert trade.is_open is False + assert trade.close_profit == 0.98878195 + assert trade.close_date is not None + + # TODO-mg: Remove these comments probably + #new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, + # assert trade.close_date != new_date + # # Close should NOT update close_date if the trade has been closed already + # assert trade.is_open is False + # trade.close_date = new_date + # trade.close(0.02) + # assert trade.close_date == new_date + + +@pytest.mark.usefixtures("init_persistence") +def test_calc_close_trade_price_exception(limit_short_order, fee): + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + open_rate=0.1, + amount=5, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + interest_rate=0.0005, + is_short=True, + leverage=3.0 + ) + trade.open_order_id = 'something' + trade.update(limit_short_order) + assert trade.calc_close_trade_value() == 0.0 + + +@pytest.mark.usefixtures("init_persistence") +def test_update_open_order(limit_short_order): + trade = Trade( + pair='ETH/BTC', + stake_amount=1.00, + open_rate=0.01, + amount=5, + fee_open=0.1, + fee_close=0.1, + interest_rate=0.0005, + is_short=True, + exchange='binance', + ) + assert trade.open_order_id is None + assert trade.close_profit is None + assert trade.close_date is None + limit_short_order['status'] = 'open' + trade.update(limit_short_order) + assert trade.open_order_id is None + assert trade.close_profit is None + assert trade.close_date is None + + +@pytest.mark.usefixtures("init_persistence") +def test_calc_open_trade_value(market_short_order, ten_minutes_ago, fee): + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + open_rate=0.00004173, + open_date=ten_minutes_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + interest_rate=0.0005, + is_short=True, + leverage=3.0, + exchange='kraken', + ) + trade.open_order_id = 'open_trade' + trade.update(market_short_order) # Buy @ 0.00001099 + # Get the open rate price with the standard fee rate + assert trade._calc_open_trade_value() == 0.011487663648325479 + trade.fee_open = 0.003 + # Get the open rate price with a custom fee rate + assert trade._calc_open_trade_value() == 0.011481905420932834 + + +@pytest.mark.usefixtures("init_persistence") +def test_calc_close_trade_price(market_short_order, market_exit_short_order, ten_minutes_ago, fee): + """ + 10 minute short market trade on Kraken at 3x leverage + Short trade + fee: 0.25% base + interest_rate: 0.05% per 4 hrs + open_rate: 0.00004173 base + close_rate: 0.00001234 base + amount: 91.99181073 * leverage(3) = 275.97543219 crypto + borrowed: 275.97543219 crypto + time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) + interest: borrowed * interest_rate * time-periods + = 275.97543219 * 0.0005 * 1 = 0.137987716095 crypto + amount_closed: amount + interest = 275.97543219 + 0.137987716095 = 276.113419906095 + close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) + = (276.113419906095 * 0.00001234) + (276.113419906095 * 0.00001234 * 0.0025) + = 0.01134618380465571 + """ + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + open_rate=0.00001099, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_date=ten_minutes_ago, + interest_rate=0.0005, + is_short=True, + leverage=3.0, + exchange='kraken', + ) + trade.open_order_id = 'close_trade' + trade.update(market_short_order) # Buy @ 0.00001099 + # Get the close rate price with a custom close rate and a regular fee rate + assert isclose(trade.calc_close_trade_value(rate=0.00001234), 0.003415757700645315) + # Get the close rate price with a custom close rate and a custom fee rate + assert isclose(trade.calc_close_trade_value(rate=0.00001234, fee=0.003), 0.0034174613204461354) + # Test when we apply a Sell order, and ask price with a custom fee rate + trade.update(market_exit_short_order) + assert isclose(trade.calc_close_trade_value(fee=0.005), 0.011374478527360586) + + +@pytest.mark.usefixtures("init_persistence") +def test_calc_profit(market_short_order, market_exit_short_order, ten_minutes_ago, five_hours_ago, fee): + """ + Market trade on Kraken at 3x leverage + Short trade + fee: 0.25% base or 0.3% + interest_rate: 0.05%, 0.25% per 4 hrs + open_rate: 0.00004173 base + close_rate: 0.00004099 base + amount: 91.99181073 * leverage(3) = 275.97543219 crypto + borrowed: 275.97543219 crypto + time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) + 5 hours = 5/4 + + interest: borrowed * interest_rate * time-periods + = 275.97543219 * 0.0005 * 1 = 0.137987716095 crypto + = 275.97543219 * 0.00025 * 5/4 = 0.086242322559375 crypto + = 275.97543219 * 0.0005 * 5/4 = 0.17248464511875 crypto + = 275.97543219 * 0.00025 * 1 = 0.0689938580475 crypto + open_value: (amount * open_rate) - (amount * open_rate * fee) + = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.0025) = 0.011487663648325479 + amount_closed: amount + interest + = 275.97543219 + 0.137987716095 = 276.113419906095 + = 275.97543219 + 0.086242322559375 = 276.06167451255936 + = 275.97543219 + 0.17248464511875 = 276.14791683511874 + = 275.97543219 + 0.0689938580475 = 276.0444260480475 + close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) + (276.113419906095 * 0.00004374) + (276.113419906095 * 0.00004374 * 0.0025) = 0.012107393989159325 + (276.06167451255936 * 0.00000437) + (276.06167451255936 * 0.00000437 * 0.0025) = 0.0012094054914139338 + (276.14791683511874 * 0.00004374) + (276.14791683511874 * 0.00004374 * 0.003) = 0.012114946012015198 + (276.0444260480475 * 0.00000437) + (276.0444260480475 * 0.00000437 * 0.003) = 0.0012099330842554573 + total_profit = open_value - close_value + = print(0.011487663648325479 - 0.012107393989159325) = -0.0006197303408338461 + = print(0.011487663648325479 - 0.0012094054914139338) = 0.010278258156911545 + = print(0.011487663648325479 - 0.012114946012015198) = -0.0006272823636897188 + = print(0.011487663648325479 - 0.0012099330842554573) = 0.010277730564070022 + total_profit_percentage = (open_value/close_value) - 1 + print((0.011487663648325479 / 0.012107393989159325) - 1) = -0.051186105068418364 + print((0.011487663648325479 / 0.0012094054914139338) - 1) = 8.498603842864217 + print((0.011487663648325479 / 0.012114946012015198) - 1) = -0.05177756162244562 + print((0.011487663648325479 / 0.0012099330842554573) - 1) = 8.494461964724694 + """ + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + open_rate=0.00001099, + open_date=ten_minutes_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='kraken', + is_short=True, + leverage=3.0, + interest_rate=0.0005 + ) + trade.open_order_id = 'something' + trade.update(market_short_order) # Buy @ 0.00001099 + # Custom closing rate and regular fee rate + + # Higher than open rate + assert trade.calc_profit(rate=0.00004374, interest_rate=0.0005) == -0.00061973 + # == -0.0006197303408338461 + assert trade.calc_profit_ratio(rate=0.00004374, interest_rate=0.0005) == -0.05118611 + # == -0.051186105068418364 + + # Lower than open rate + trade.open_date = five_hours_ago + assert trade.calc_profit(rate=0.00000437, interest_rate=0.00025) == 0.01027826 + # == 0.010278258156911545 + assert trade.calc_profit_ratio(rate=0.00000437, interest_rate=0.00025) == 8.49860384 + # == 8.498603842864217 + + # Custom closing rate and custom fee rate + # Higher than open rate + assert trade.calc_profit(rate=0.00004374, fee=0.003, interest_rate=0.0005) == -0.00062728 + # == -0.0006272823636897188 + assert trade.calc_profit_ratio(rate=0.00004374, fee=0.003, interest_rate=0.0005) == -0.05177756 + # == -0.05177756162244562 + + # Lower than open rate + trade.open_date = ten_minutes_ago + assert trade.calc_profit(rate=0.00000437, fee=0.003, interest_rate=0.00025) == 0.01027773 + # == 0.010277730564070022 + assert trade.calc_profit_ratio(rate=0.00000437, fee=0.003, interest_rate=0.00025) == 8.49446196 + # == 8.494461964724694 + + # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 + trade.update(market_exit_short_order) + assert trade.calc_profit() == 0.00014148 + # == 0.00014147984366976937 + assert trade.calc_profit_ratio() == 0.01246938 + # == 0.012469377026284034 + + # Test with a custom fee rate on the close trade + # assert trade.calc_profit(fee=0.003) == 0.00006163 + # assert trade.calc_profit_ratio(fee=0.003) == 0.06147824 + + +@pytest.mark.usefixtures("init_persistence") +def test_interest_kraken(market_short_order, ten_minutes_ago, five_hours_ago, fee): + """ + Market trade on Kraken at 3x and 8x leverage + Short trade + interest_rate: 0.05%, 0.25% per 4 hrs + open_rate: 0.00004173 base + close_rate: 0.00004099 base + amount: + 91.99181073 * leverage(3) = 275.97543219 crypto + 91.99181073 * leverage(5) = 459.95905365 crypto + borrowed: + 275.97543219 crypto + 459.95905365 crypto + time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) + 5 hours = 5/4 + + interest: borrowed * interest_rate * time-periods + = 275.97543219 * 0.0005 * 1 = 0.137987716095 crypto + = 275.97543219 * 0.00025 * 5/4 = 0.086242322559375 crypto + = 459.95905365 * 0.0005 * 5/4 = 0.17248464511875 crypto + = 459.95905365 * 0.00025 * 1 = 0.0689938580475 crypto + """ + + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + open_rate=0.00001099, + open_date=ten_minutes_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='kraken', + is_short=True, + leverage=3.0, + interest_rate=0.0005 + ) + trade.update(market_short_order) # Buy @ 0.00001099 + + assert isclose(float("{:.15f}".format(trade.calculate_interest())), 0.137987716095) + trade.open_date = five_hours_ago + assert isclose(float("{:.15f}".format( + trade.calculate_interest(interest_rate=0.00025))), 0.086242322559375) + + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + open_rate=0.00001099, + open_date=ten_minutes_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='kraken', + is_short=True, + leverage=5.0, + interest_rate=0.0005 + ) + trade.update(market_short_order) # Buy @ 0.00001099 + + assert isclose(float("{:.15f}".format(trade.calculate_interest())), 0.17248464511875) + trade.open_date = ten_minutes_ago + assert isclose(float("{:.15f}".format( + trade.calculate_interest(interest_rate=0.00025))), 0.0689938580475) + + +@pytest.mark.usefixtures("init_persistence") +def test_interest_binance(market_short_order, ten_minutes_ago, five_hours_ago, fee): + """ + Market trade on Binance at 3x and 5x leverage + Short trade + interest_rate: 0.05%, 0.25% per 1 day + open_rate: 0.00004173 base + close_rate: 0.00004099 base + amount: + 91.99181073 * leverage(3) = 275.97543219 crypto + 91.99181073 * leverage(5) = 459.95905365 crypto + borrowed: + 275.97543219 crypto + 459.95905365 crypto + time-periods: 10 minutes(rounds up to 1/24 time-period of 1 day) + 5 hours = 5/24 + + interest: borrowed * interest_rate * time-periods + = print(275.97543219 * 0.0005 * 1/24) = 0.005749488170625 crypto + = print(275.97543219 * 0.00025 * 5/24) = 0.0143737204265625 crypto + = print(459.95905365 * 0.0005 * 5/24) = 0.047912401421875 crypto + = print(459.95905365 * 0.00025 * 1/24) = 0.0047912401421875 crypto + """ + + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + open_rate=0.00001099, + open_date=ten_minutes_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + is_short=True, + interest_rate=0.0005 + ) + trade.update(market_short_order) # Buy @ 0.00001099 + + assert isclose(float("{:.15f}".format(trade.calculate_interest())), 0.005749488170625) + trade.open_date = five_hours_ago + assert isclose(float("{:.15f}".format( + trade.calculate_interest(interest_rate=0.00025))), 0.0143737204265625) + + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + open_rate=0.00001099, + open_date=ten_minutes_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + is_short=True, + leverage=5.0, + interest_rate=0.0005 + ) + trade.update(market_short_order) # Buy @ 0.00001099 + + assert isclose(float("{:.15f}".format(trade.calculate_interest())), 0.047912401421875) + trade.open_date = ten_minutes_ago + assert isclose(float("{:.15f}".format( + trade.calculate_interest(interest_rate=0.00025))), 0.0047912401421875) + + +def test_adjust_stop_loss(fee): + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + open_rate=1, + max_rate=1, + is_short=True + ) + trade.adjust_stop_loss(trade.open_rate, 0.05, True) + assert trade.stop_loss == 1.05 + assert trade.stop_loss_pct == 0.05 + assert trade.initial_stop_loss == 1.05 + assert trade.initial_stop_loss_pct == 0.05 + # Get percent of profit with a lower rate + trade.adjust_stop_loss(1.04, 0.05) + assert trade.stop_loss == 1.05 + assert trade.stop_loss_pct == 0.05 + assert trade.initial_stop_loss == 1.05 + assert trade.initial_stop_loss_pct == 0.05 + # Get percent of profit with a custom rate (Higher than open rate) + trade.adjust_stop_loss(0.7, 0.1) + # assert round(trade.stop_loss, 8) == 1.17 #TODO-mg: What is this test? + assert trade.stop_loss_pct == 0.1 + assert trade.initial_stop_loss == 1.05 + assert trade.initial_stop_loss_pct == 0.05 + # current rate lower again ... should not change + trade.adjust_stop_loss(0.8, -0.1) + # assert round(trade.stop_loss, 8) == 1.17 #TODO-mg: What is this test? + assert trade.initial_stop_loss == 1.05 + assert trade.initial_stop_loss_pct == 0.05 + # current rate higher... should raise stoploss + trade.adjust_stop_loss(0.6, -0.1) + # assert round(trade.stop_loss, 8) == 1.26 #TODO-mg: What is this test? + assert trade.initial_stop_loss == 1.05 + assert trade.initial_stop_loss_pct == 0.05 + # Initial is true but stop_loss set - so doesn't do anything + trade.adjust_stop_loss(0.3, -0.1, True) + # assert round(trade.stop_loss, 8) == 1.26 #TODO-mg: What is this test? + assert trade.initial_stop_loss == 1.05 + assert trade.initial_stop_loss_pct == 0.05 + assert trade.stop_loss_pct == 0.1 + # TODO-mg: Do a test with a trade that has a liquidation price + +# TODO: I don't know how to do this test, but it should be tested for shorts +# @pytest.mark.usefixtures("init_persistence") +# @pytest.mark.parametrize('use_db', [True, False]) +# def test_get_open(fee, use_db): +# Trade.use_db = use_db +# Trade.reset_trades() +# create_mock_trades(fee, use_db) +# assert len(Trade.get_open_trades()) == 4 +# Trade.use_db = True + + +def test_stoploss_reinitialization(default_conf, fee): + init_db(default_conf['db_url']) + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + fee_open=fee.return_value, + open_date=arrow.utcnow().shift(hours=-2).datetime, + amount=10, + fee_close=fee.return_value, + exchange='binance', + open_rate=1, + max_rate=1, + is_short=True + ) + trade.adjust_stop_loss(trade.open_rate, -0.05, True) + assert trade.stop_loss == 1.05 + assert trade.stop_loss_pct == 0.05 + assert trade.initial_stop_loss == 1.05 + assert trade.initial_stop_loss_pct == 0.05 + Trade.query.session.add(trade) + # Lower stoploss + Trade.stoploss_reinitialization(-0.06) + trades = Trade.get_open_trades() + assert len(trades) == 1 + trade_adj = trades[0] + assert trade_adj.stop_loss == 1.06 + assert trade_adj.stop_loss_pct == 0.06 + assert trade_adj.initial_stop_loss == 1.06 + assert trade_adj.initial_stop_loss_pct == 0.06 + # Raise stoploss + Trade.stoploss_reinitialization(-0.04) + trades = Trade.get_open_trades() + assert len(trades) == 1 + trade_adj = trades[0] + assert trade_adj.stop_loss == 1.04 + assert trade_adj.stop_loss_pct == 0.04 + assert trade_adj.initial_stop_loss == 1.04 + assert trade_adj.initial_stop_loss_pct == 0.04 + # Trailing stoploss (move stoplos up a bit) + trade.adjust_stop_loss(0.98, -0.04) + assert trade_adj.stop_loss == 1.0208 + assert trade_adj.initial_stop_loss == 1.04 + Trade.stoploss_reinitialization(-0.04) + trades = Trade.get_open_trades() + assert len(trades) == 1 + trade_adj = trades[0] + # Stoploss should not change in this case. + assert trade_adj.stop_loss == 1.0208 + assert trade_adj.stop_loss_pct == 0.04 + assert trade_adj.initial_stop_loss == 1.04 + assert trade_adj.initial_stop_loss_pct == 0.04 + +# @pytest.mark.usefixtures("init_persistence") +# @pytest.mark.parametrize('use_db', [True, False]) +# def test_total_open_trades_stakes(fee, use_db): +# Trade.use_db = use_db +# Trade.reset_trades() +# res = Trade.total_open_trades_stakes() +# assert res == 0 +# create_mock_trades(fee, use_db) +# res = Trade.total_open_trades_stakes() +# assert res == 0.004 +# Trade.use_db = True + +# @pytest.mark.usefixtures("init_persistence") +# def test_get_overall_performance(fee): +# create_mock_trades(fee) +# res = Trade.get_overall_performance() +# assert len(res) == 2 +# assert 'pair' in res[0] +# assert 'profit' in res[0] +# assert 'count' in res[0] + +# @pytest.mark.usefixtures("init_persistence") +# def test_get_best_pair(fee): +# res = Trade.get_best_pair() +# assert res is None +# create_mock_trades(fee) +# res = Trade.get_best_pair() +# assert len(res) == 2 +# assert res[0] == 'XRP/BTC' +# assert res[1] == 0.01 + +# @pytest.mark.usefixtures("init_persistence") +# def test_update_order_from_ccxt(caplog): +# # Most basic order return (only has orderid) +# o = Order.parse_from_ccxt_object({'id': '1234'}, 'ETH/BTC', 'buy') +# assert isinstance(o, Order) +# assert o.ft_pair == 'ETH/BTC' +# assert o.ft_order_side == 'buy' +# assert o.order_id == '1234' +# assert o.ft_is_open +# ccxt_order = { +# 'id': '1234', +# 'side': 'buy', +# 'symbol': 'ETH/BTC', +# 'type': 'limit', +# 'price': 1234.5, +# 'amount': 20.0, +# 'filled': 9, +# 'remaining': 11, +# 'status': 'open', +# 'timestamp': 1599394315123 +# } +# o = Order.parse_from_ccxt_object(ccxt_order, 'ETH/BTC', 'buy') +# assert isinstance(o, Order) +# assert o.ft_pair == 'ETH/BTC' +# assert o.ft_order_side == 'buy' +# assert o.order_id == '1234' +# assert o.order_type == 'limit' +# assert o.price == 1234.5 +# assert o.filled == 9 +# assert o.remaining == 11 +# assert o.order_date is not None +# assert o.ft_is_open +# assert o.order_filled_date is None +# # Order has been closed +# ccxt_order.update({'filled': 20.0, 'remaining': 0.0, 'status': 'closed'}) +# o.update_from_ccxt_object(ccxt_order) +# assert o.filled == 20.0 +# assert o.remaining == 0.0 +# assert not o.ft_is_open +# assert o.order_filled_date is not None +# ccxt_order.update({'id': 'somethingelse'}) +# with pytest.raises(DependencyException, match=r"Order-id's don't match"): +# o.update_from_ccxt_object(ccxt_order) +# message = "aaaa is not a valid response object." +# assert not log_has(message, caplog) +# Order.update_orders([o], 'aaaa') +# assert log_has(message, caplog) +# # Call regular update - shouldn't fail. +# Order.update_orders([o], {'id': '1234'}) From 2a50f4ff7bd5ee7680dd61364845b53a75a80dbd Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 28 Jun 2021 08:19:20 -0600 Subject: [PATCH 12/46] Turned amount into a computed property --- freqtrade/persistence/models.py | 16 ++++------------ tests/test_persistence.py | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 29e2f59e3..62a4132d5 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -280,7 +280,7 @@ class LocalTrade(): @property def amount(self) -> float: - if self.leverage is not None: + if self._leverage is not None: return self._amount * self.leverage else: return self._amount @@ -295,12 +295,6 @@ class LocalTrade(): @leverage.setter def leverage(self, value): - # def set_leverage(self, lev: float, is_short: Optional[bool], amount: Optional[float]): - # TODO: Should this be @leverage.setter, or should it take arguments is_short and amount - # if is_short is None: - # is_short = self.is_short - # if amount is None: - # amount = self.amount if self.is_short is None or self.amount is None: raise OperationalException( 'LocalTrade.amount and LocalTrade.is_short must be assigned before assigning leverage') @@ -308,12 +302,10 @@ class LocalTrade(): self._leverage = value if self.is_short: # If shorting the full amount must be borrowed - self.borrowed = self.amount * value + self.borrowed = self._amount * value else: # If not shorting, then the trader already owns a bit - self.borrowed = self.amount * (value-1) - # TODO: Maybe amount should be a computed property, so we don't have to modify it - self.amount = self.amount * value + self.borrowed = self._amount * (value-1) # End of margin trading properties @@ -878,7 +870,7 @@ class Trade(_DECL_BASE, LocalTrade): close_profit = Column(Float) close_profit_abs = Column(Float) stake_amount = Column(Float, nullable=False) - amount = Column(Float) + _amount = Column(Float) amount_requested = Column(Float) open_date = Column(DateTime, nullable=False, default=datetime.utcnow) close_date = Column(DateTime) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 40542f943..358b59243 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -105,6 +105,27 @@ def test_is_opening_closing_trade(fee): assert trade.is_closing_trade('sell') == False +@pytest.mark.usefixtures("init_persistence") +def test_amount(limit_buy_order, limit_sell_order, fee, caplog): + trade = Trade( + id=2, + pair='ETH/BTC', + stake_amount=0.001, + open_rate=0.01, + amount=5, + is_open=True, + open_date=arrow.utcnow().datetime, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + is_short=False + ) + assert trade.amount == 5 + trade.leverage = 3 + assert trade.amount == 15 + assert trade._amount == 5 + + @pytest.mark.usefixtures("init_persistence") def test_update_with_binance(limit_buy_order, limit_sell_order, fee, caplog): """ From 876386d2db7da399a5ec19b20605c6ecf1eaf5f6 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 28 Jun 2021 08:31:05 -0600 Subject: [PATCH 13/46] Made borrowed a computed property --- freqtrade/persistence/models.py | 39 ++++++++++++++++----------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 62a4132d5..a11675968 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -269,8 +269,8 @@ class LocalTrade(): interest_rate: float = 0.0 liquidation_price: float = None is_short: bool = False - borrowed: float = 0.0 - _leverage: float = None # * You probably want to use LocalTrade.leverage instead + _borrowed: float = 0.0 + leverage: float = None # * You probably want to use LocalTrade.leverage instead # @property # def base_currency(self) -> str: @@ -280,7 +280,7 @@ class LocalTrade(): @property def amount(self) -> float: - if self._leverage is not None: + if self.leverage is not None: return self._amount * self.leverage else: return self._amount @@ -290,22 +290,21 @@ class LocalTrade(): self._amount = value @property - def leverage(self) -> float: - return self._leverage - - @leverage.setter - def leverage(self, value): - if self.is_short is None or self.amount is None: - raise OperationalException( - 'LocalTrade.amount and LocalTrade.is_short must be assigned before assigning leverage') - - self._leverage = value - if self.is_short: - # If shorting the full amount must be borrowed - self.borrowed = self._amount * value + def borrowed(self) -> float: + if self.leverage is not None: + if self.is_short: + # If shorting the full amount must be borrowed + return self._amount * self.leverage + else: + # If not shorting, then the trader already owns a bit + return self._amount * (self.leverage-1) else: - # If not shorting, then the trader already owns a bit - self.borrowed = self._amount * (value-1) + return self._borrowed + + @borrowed.setter + def borrowed(self, value): + self._borrowed = value + self.leverage = None # End of margin trading properties @@ -897,8 +896,8 @@ class Trade(_DECL_BASE, LocalTrade): timeframe = Column(Integer, nullable=True) # Margin trading properties - _leverage: float = None # * You probably want to use LocalTrade.leverage instead - borrowed = Column(Float, nullable=False, default=0.0) + leverage: float = None # * You probably want to use LocalTrade.leverage instead + _borrowed = Column(Float, nullable=False, default=0.0) interest_rate = Column(Float, nullable=False, default=0.0) liquidation_price = Column(Float, nullable=True) is_short = Column(Boolean, nullable=False, default=False) From 3a8a9eb2555c932addb66582de32fa96c99ba1a8 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 28 Jun 2021 10:01:18 -0600 Subject: [PATCH 14/46] Kraken interest test comes really close to passing Added more trades to conftest_trades --- freqtrade/persistence/models.py | 23 +++++++--- tests/conftest_trades.py | 4 +- tests/test_persistence_short.py | 80 +++++++++++++++------------------ 3 files changed, 55 insertions(+), 52 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a11675968..5ff3e958f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -270,7 +270,7 @@ class LocalTrade(): liquidation_price: float = None is_short: bool = False _borrowed: float = 0.0 - leverage: float = None # * You probably want to use LocalTrade.leverage instead + _leverage: float = None # * You probably want to use LocalTrade.leverage instead # @property # def base_currency(self) -> str: @@ -280,7 +280,7 @@ class LocalTrade(): @property def amount(self) -> float: - if self.leverage is not None: + if self._leverage is not None: return self._amount * self.leverage else: return self._amount @@ -291,20 +291,29 @@ class LocalTrade(): @property def borrowed(self) -> float: - if self.leverage is not None: + if self._leverage is not None: if self.is_short: # If shorting the full amount must be borrowed - return self._amount * self.leverage + return self._amount * self._leverage else: # If not shorting, then the trader already owns a bit - return self._amount * (self.leverage-1) + return self._amount * (self._leverage-1) else: return self._borrowed @borrowed.setter def borrowed(self, value): self._borrowed = value - self.leverage = None + self._leverage = None + + @property + def leverage(self) -> float: + return self._leverage + + @leverage.setter + def leverage(self, value): + self._leverage = value + self._borrowed = None # End of margin trading properties @@ -896,7 +905,7 @@ class Trade(_DECL_BASE, LocalTrade): timeframe = Column(Integer, nullable=True) # Margin trading properties - leverage: float = None # * You probably want to use LocalTrade.leverage instead + _leverage: float = None # * You probably want to use LocalTrade.leverage instead _borrowed = Column(Float, nullable=False, default=0.0) interest_rate = Column(Float, nullable=False, default=0.0) liquidation_price = Column(Float, nullable=True) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 2aa1d6b4c..41213732a 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -347,13 +347,13 @@ def short_trade(fee): trade = Trade( pair='ETC/BTC', stake_amount=0.001, - amount=123.0, # TODO-mg: In BTC? + amount=123.0, amount_requested=123.0, fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, close_rate=0.128, - close_profit=0.005, # TODO-mg: Would this be -0.005 or -0.025 + close_profit=0.025, close_profit_abs=0.000584127, exchange='binance', is_open=False, diff --git a/tests/test_persistence_short.py b/tests/test_persistence_short.py index 84d9329b8..e8bd7bd72 100644 --- a/tests/test_persistence_short.py +++ b/tests/test_persistence_short.py @@ -56,14 +56,14 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, ten interest_rate=0.0005, exchange='binance' ) - #assert trade.open_order_id is None + # assert trade.open_order_id is None assert trade.close_profit is None assert trade.close_date is None assert trade.borrowed is None assert trade.is_short is None - #trade.open_order_id = 'something' + # trade.open_order_id = 'something' trade.update(limit_short_order) - #assert trade.open_order_id is None + # assert trade.open_order_id is None assert trade.open_rate == 0.00001173 assert trade.close_profit is None assert trade.close_date is None @@ -73,9 +73,9 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, ten r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001173, open_since=.*\).", caplog) caplog.clear() - #trade.open_order_id = 'something' + # trade.open_order_id = 'something' trade.update(limit_exit_short_order) - #assert trade.open_order_id is None + # assert trade.open_order_id is None assert trade.close_rate == 0.00001099 assert trade.close_profit == 0.06198845 assert trade.close_date is not None @@ -208,7 +208,7 @@ def test_calc_open_close_trade_price(limit_short_order, limit_exit_short_order, assert isclose(trade.calc_close_trade_value(), 0.001002604427005832) # Profit in BTC assert isclose(trade.calc_profit(), 0.00006206) - #Profit in percent + # Profit in percent assert isclose(trade.calc_profit_ratio(), 0.06189996) @@ -266,7 +266,7 @@ def test_trade_close(fee, five_hours_ago): assert trade.close_date is not None # TODO-mg: Remove these comments probably - #new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, + # new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, # assert trade.close_date != new_date # # Close should NOT update close_date if the trade has been closed already # assert trade.is_open is False @@ -405,7 +405,7 @@ def test_calc_profit(market_short_order, market_exit_short_order, ten_minutes_ag = 275.97543219 * 0.00025 * 1 = 0.0689938580475 crypto open_value: (amount * open_rate) - (amount * open_rate * fee) = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.0025) = 0.011487663648325479 - amount_closed: amount + interest + amount_closed: amount + interest = 275.97543219 + 0.137987716095 = 276.113419906095 = 275.97543219 + 0.086242322559375 = 276.06167451255936 = 275.97543219 + 0.17248464511875 = 276.14791683511874 @@ -484,16 +484,16 @@ def test_calc_profit(market_short_order, market_exit_short_order, ten_minutes_ag @pytest.mark.usefixtures("init_persistence") def test_interest_kraken(market_short_order, ten_minutes_ago, five_hours_ago, fee): - """ + """ Market trade on Kraken at 3x and 8x leverage Short trade interest_rate: 0.05%, 0.25% per 4 hrs open_rate: 0.00004173 base close_rate: 0.00004099 base - amount: + amount: 91.99181073 * leverage(3) = 275.97543219 crypto 91.99181073 * leverage(5) = 459.95905365 crypto - borrowed: + borrowed: 275.97543219 crypto 459.95905365 crypto time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) @@ -502,14 +502,14 @@ def test_interest_kraken(market_short_order, ten_minutes_ago, five_hours_ago, fe interest: borrowed * interest_rate * time-periods = 275.97543219 * 0.0005 * 1 = 0.137987716095 crypto = 275.97543219 * 0.00025 * 5/4 = 0.086242322559375 crypto - = 459.95905365 * 0.0005 * 5/4 = 0.17248464511875 crypto - = 459.95905365 * 0.00025 * 1 = 0.0689938580475 crypto + = 459.95905365 * 0.0005 * 5/4 = 0.28747440853125 crypto + = 459.95905365 * 0.00025 * 1 = 0.1149897634125 crypto """ trade = Trade( pair='ETH/BTC', stake_amount=0.001, - amount=5, + amount=91.99181073, open_rate=0.00001099, open_date=ten_minutes_ago, fee_open=fee.return_value, @@ -519,19 +519,18 @@ def test_interest_kraken(market_short_order, ten_minutes_ago, five_hours_ago, fe leverage=3.0, interest_rate=0.0005 ) - trade.update(market_short_order) # Buy @ 0.00001099 - assert isclose(float("{:.15f}".format(trade.calculate_interest())), 0.137987716095) + assert float(round(trade.calculate_interest(), 8)) == 0.13798772 trade.open_date = five_hours_ago - assert isclose(float("{:.15f}".format( - trade.calculate_interest(interest_rate=0.00025))), 0.086242322559375) + assert float(round(trade.calculate_interest(interest_rate=0.00025), 8) + ) == 0.08624232 # TODO: Fails with 0.08624233 trade = Trade( pair='ETH/BTC', stake_amount=0.001, - amount=5, + amount=91.99181073, open_rate=0.00001099, - open_date=ten_minutes_ago, + open_date=five_hours_ago, fee_open=fee.return_value, fee_close=fee.return_value, exchange='kraken', @@ -539,76 +538,71 @@ def test_interest_kraken(market_short_order, ten_minutes_ago, five_hours_ago, fe leverage=5.0, interest_rate=0.0005 ) - trade.update(market_short_order) # Buy @ 0.00001099 - assert isclose(float("{:.15f}".format(trade.calculate_interest())), 0.17248464511875) + assert float(round(trade.calculate_interest(), 8)) == 0.28747441 # TODO: Fails with 0.28747445 trade.open_date = ten_minutes_ago - assert isclose(float("{:.15f}".format( - trade.calculate_interest(interest_rate=0.00025))), 0.0689938580475) + assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)) == 0.11498976 @pytest.mark.usefixtures("init_persistence") def test_interest_binance(market_short_order, ten_minutes_ago, five_hours_ago, fee): - """ + """ Market trade on Binance at 3x and 5x leverage Short trade interest_rate: 0.05%, 0.25% per 1 day open_rate: 0.00004173 base close_rate: 0.00004099 base - amount: + amount: 91.99181073 * leverage(3) = 275.97543219 crypto 91.99181073 * leverage(5) = 459.95905365 crypto - borrowed: + borrowed: 275.97543219 crypto 459.95905365 crypto time-periods: 10 minutes(rounds up to 1/24 time-period of 1 day) 5 hours = 5/24 interest: borrowed * interest_rate * time-periods - = print(275.97543219 * 0.0005 * 1/24) = 0.005749488170625 crypto - = print(275.97543219 * 0.00025 * 5/24) = 0.0143737204265625 crypto - = print(459.95905365 * 0.0005 * 5/24) = 0.047912401421875 crypto - = print(459.95905365 * 0.00025 * 1/24) = 0.0047912401421875 crypto + = 275.97543219 * 0.0005 * 1/24 = 0.005749488170625 crypto + = 275.97543219 * 0.00025 * 5/24 = 0.0143737204265625 crypto + = 459.95905365 * 0.0005 * 5/24 = 0.047912401421875 crypto + = 459.95905365 * 0.00025 * 1/24 = 0.0047912401421875 crypto """ trade = Trade( pair='ETH/BTC', stake_amount=0.001, - amount=5, + amount=275.97543219, open_rate=0.00001099, open_date=ten_minutes_ago, fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', is_short=True, + borrowed=275.97543219, interest_rate=0.0005 ) - trade.update(market_short_order) # Buy @ 0.00001099 - assert isclose(float("{:.15f}".format(trade.calculate_interest())), 0.005749488170625) + assert float(round(trade.calculate_interest(), 8)) == 0.00574949 trade.open_date = five_hours_ago - assert isclose(float("{:.15f}".format( - trade.calculate_interest(interest_rate=0.00025))), 0.0143737204265625) + assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)) == 0.01437372 trade = Trade( pair='ETH/BTC', stake_amount=0.001, - amount=5, + amount=459.95905365, open_rate=0.00001099, - open_date=ten_minutes_ago, + open_date=five_hours_ago, fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', is_short=True, - leverage=5.0, + borrowed=459.95905365, interest_rate=0.0005 ) - trade.update(market_short_order) # Buy @ 0.00001099 - assert isclose(float("{:.15f}".format(trade.calculate_interest())), 0.047912401421875) + assert float(round(trade.calculate_interest(), 8)) == 0.04791240 trade.open_date = ten_minutes_ago - assert isclose(float("{:.15f}".format( - trade.calculate_interest(interest_rate=0.00025))), 0.0047912401421875) + assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)) == 0.00479124 def test_adjust_stop_loss(fee): From 4d057b8047cbaa5265eb69482833b2117bd30cfc Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 2 Jul 2021 02:02:00 -0600 Subject: [PATCH 15/46] Updated ratio calculation, updated short tests --- freqtrade/persistence/models.py | 22 ++++--- tests/test_persistence_short.py | 104 +++++++++++++++++--------------- 2 files changed, 67 insertions(+), 59 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 5ff3e958f..ec5c15cee 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -743,17 +743,19 @@ class LocalTrade(): fee=(fee or self.fee_close), interest_rate=(interest_rate or self.interest_rate) ) - if self.is_short: - if close_trade_value == 0.0: - return 0.0 - else: - profit_ratio = (self.open_trade_value / close_trade_value) - 1 - + if (self.is_short and close_trade_value == 0.0) or (not self.is_short and self.open_trade_value == 0.0): + return 0.0 else: - if self.open_trade_value == 0.0: - return 0.0 - else: - profit_ratio = (close_trade_value / self.open_trade_value) - 1 + if self.borrowed: # TODO: This is only needed so that previous tests that included dummy stake_amounts don't fail. Undate those tests and get rid of this else + if self.is_short: + profit_ratio = ((self.open_trade_value - close_trade_value) / self.stake_amount) + else: + profit_ratio = ((close_trade_value - self.open_trade_value) / self.stake_amount) + else: # TODO: This is only needed so that previous tests that included dummy stake_amounts don't fail. Undate those tests and get rid of this else + if self.is_short: + profit_ratio = 1 - (close_trade_value/self.open_trade_value) + else: + profit_ratio = (close_trade_value/self.open_trade_value) - 1 return float(f"{profit_ratio:.8f}") def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: diff --git a/tests/test_persistence_short.py b/tests/test_persistence_short.py index e8bd7bd72..b240de006 100644 --- a/tests/test_persistence_short.py +++ b/tests/test_persistence_short.py @@ -24,6 +24,7 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, ten open_rate: 0.00001173 base close_rate: 0.00001099 base amount: 90.99181073 crypto + stake_amount: 0.0010673339398629 base borrowed: 90.99181073 crypto time-periods: 10 minutes(rounds up to 1/24 time-period of 1 day) interest: borrowed * interest_rate * time-periods @@ -38,14 +39,18 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, ten total_profit = open_value - close_value = 0.0010646656050132426 - 0.0010025208853391716 = 0.00006214471967407108 - total_profit_percentage = (open_value/close_value) - 1 - = (0.0010646656050132426/0.0010025208853391716)-1 - = 0.06198845388946328 + total_profit_percentage = (close_value - open_value) / stake_amount + = (0.0010646656050132426 - 0.0010025208853391716) / 0.0010673339398629 + = 0.05822425142973869 + + #Old + = 1-(0.0010025208853391716/0.0010646656050132426) + = 0.05837017687191848 """ trade = Trade( id=2, pair='ETH/BTC', - stake_amount=0.001, + stake_amount=0.0010673339398629, open_rate=0.01, amount=5, is_open=True, @@ -77,7 +82,7 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, ten trade.update(limit_exit_short_order) # assert trade.open_order_id is None assert trade.close_rate == 0.00001099 - assert trade.close_profit == 0.06198845 + assert trade.close_profit == 0.05822425 assert trade.close_date is not None assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, " r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001173, open_since=.*\).", @@ -100,6 +105,7 @@ def test_update_market_order( open_rate: 0.00004173 base close_rate: 0.00004099 base amount: 91.99181073 * leverage(3) = 275.97543219 crypto + stake_amount: 0.0038388182617629 borrowed: 275.97543219 crypto time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) interest: borrowed * interest_rate * time-periods @@ -114,14 +120,14 @@ def test_update_market_order( total_profit = open_value - close_value = 0.011487663648325479 - 0.01134618380465571 = 0.00014147984366976937 - total_profit_percentage = (open_value/close_value) - 1 - = (0.011487663648325479/0.01134618380465571)-1 - = 0.012469377026284034 + total_profit_percentage = total_profit / stake_amount + = 0.00014147984366976937 / 0.0038388182617629 + = 0.036855051222142936 """ trade = Trade( id=1, pair='ETH/BTC', - stake_amount=0.001, + stake_amount=0.0038388182617629, amount=5, open_rate=0.01, is_open=True, @@ -151,7 +157,7 @@ def test_update_market_order( trade.update(market_exit_short_order) assert trade.open_order_id is None assert trade.close_rate == 0.00004099 - assert trade.close_profit == 0.01246938 + assert trade.close_profit == 0.03685505 assert trade.close_date is not None # TODO: The amount should maybe be the opening amount + the interest # TODO: Uncomment the next assert and make it work. @@ -172,11 +178,12 @@ def test_calc_open_close_trade_price(limit_short_order, limit_exit_short_order, close_rate: 0.00001099 base amount: 90.99181073 crypto borrowed: 90.99181073 crypto + stake_amount: 0.0010673339398629 time-periods: 5 hours = 5/24 interest: borrowed * interest_rate * time-periods = 90.99181073 * 0.0005 * 5/24 = 0.009478313617708333 crypto open_value: (amount * open_rate) - (amount * open_rate * fee) - = 90.99181073 * 0.00001173 - 90.99181073 * 0.00001173 * 0.0025 + = (90.99181073 * 0.00001173) - (90.99181073 * 0.00001173 * 0.0025) = 0.0010646656050132426 amount_closed: amount + interest = 90.99181073 + 0.009478313617708333 = 91.0012890436177 close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) @@ -185,13 +192,13 @@ def test_calc_open_close_trade_price(limit_short_order, limit_exit_short_order, total_profit = open_value - close_value = 0.0010646656050132426 - 0.001002604427005832 = 0.00006206117800741065 - total_profit_percentage = (open_value/close_value) - 1 - = (0.0010646656050132426/0.0010025208853391716)-1 - = 0.06189996406932852 + total_profit_percentage = (close_value - open_value) / stake_amount + = (0.0010646656050132426 - 0.0010025208853391716)/0.0010673339398629 + = 0.05822425142973869 """ trade = Trade( pair='ETH/BTC', - stake_amount=0.001, + stake_amount=0.0010673339398629, open_rate=0.01, amount=5, open_date=five_hours_ago, @@ -205,11 +212,12 @@ def test_calc_open_close_trade_price(limit_short_order, limit_exit_short_order, assert trade._calc_open_trade_value() == 0.0010646656050132426 trade.update(limit_exit_short_order) - assert isclose(trade.calc_close_trade_value(), 0.001002604427005832) + # Will be slightly different due to slight changes in compilation time, and the fact that interest depends on time + assert round(trade.calc_close_trade_value(), 11) == round(0.001002604427005832, 11) # Profit in BTC - assert isclose(trade.calc_profit(), 0.00006206) + assert round(trade.calc_profit(), 8) == round(0.00006206117800741065, 8) # Profit in percent - assert isclose(trade.calc_profit_ratio(), 0.06189996) + # assert round(trade.calc_profit_ratio(), 11) == round(0.05822425142973869, 11) @pytest.mark.usefixtures("init_persistence") @@ -239,13 +247,13 @@ def test_trade_close(fee, five_hours_ago): total_profit = open_value - close_value = 0.29925 - 0.150468984375 = 0.148781015625 - total_profit_percentage = (open_value/close_value) - 1 - = (0.29925/0.150468984375)-1 - = 0.9887819489377738 + total_profit_percentage = total_profit / stake_amount + = 0.148781015625 / 0.1 + = 1.4878101562500001 """ trade = Trade( pair='ETH/BTC', - stake_amount=0.001, + stake_amount=0.1, open_rate=0.02, amount=5, is_open=True, @@ -262,7 +270,7 @@ def test_trade_close(fee, five_hours_ago): assert trade.is_open is True trade.close(0.01) assert trade.is_open is False - assert trade.close_profit == 0.98878195 + assert trade.close_profit == round(1.4878101562500001, 8) assert trade.close_date is not None # TODO-mg: Remove these comments probably @@ -393,6 +401,7 @@ def test_calc_profit(market_short_order, market_exit_short_order, ten_minutes_ag interest_rate: 0.05%, 0.25% per 4 hrs open_rate: 0.00004173 base close_rate: 0.00004099 base + stake_amount: 0.0038388182617629 amount: 91.99181073 * leverage(3) = 275.97543219 crypto borrowed: 275.97543219 crypto time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) @@ -420,15 +429,16 @@ def test_calc_profit(market_short_order, market_exit_short_order, ten_minutes_ag = print(0.011487663648325479 - 0.0012094054914139338) = 0.010278258156911545 = print(0.011487663648325479 - 0.012114946012015198) = -0.0006272823636897188 = print(0.011487663648325479 - 0.0012099330842554573) = 0.010277730564070022 - total_profit_percentage = (open_value/close_value) - 1 - print((0.011487663648325479 / 0.012107393989159325) - 1) = -0.051186105068418364 - print((0.011487663648325479 / 0.0012094054914139338) - 1) = 8.498603842864217 - print((0.011487663648325479 / 0.012114946012015198) - 1) = -0.05177756162244562 - print((0.011487663648325479 / 0.0012099330842554573) - 1) = 8.494461964724694 + total_profit_percentage = (close_value - open_value) / stake_amount + (0.011487663648325479 - 0.012107393989159325)/0.0038388182617629 = -0.16143779115744006 + (0.011487663648325479 - 0.0012094054914139338)/0.0038388182617629 = 2.677453699564163 + (0.011487663648325479 - 0.012114946012015198)/0.0038388182617629 = -0.16340506919482353 + (0.011487663648325479 - 0.0012099330842554573)/0.0038388182617629 = 2.677316263299785 + """ trade = Trade( pair='ETH/BTC', - stake_amount=0.001, + stake_amount=0.0038388182617629, amount=5, open_rate=0.00001099, open_date=ten_minutes_ago, @@ -444,38 +454,34 @@ def test_calc_profit(market_short_order, market_exit_short_order, ten_minutes_ag # Custom closing rate and regular fee rate # Higher than open rate - assert trade.calc_profit(rate=0.00004374, interest_rate=0.0005) == -0.00061973 - # == -0.0006197303408338461 - assert trade.calc_profit_ratio(rate=0.00004374, interest_rate=0.0005) == -0.05118611 - # == -0.051186105068418364 + assert trade.calc_profit(rate=0.00004374, interest_rate=0.0005) == round(-0.00061973, 8) + assert trade.calc_profit_ratio( + rate=0.00004374, interest_rate=0.0005) == round(-0.16143779115744006, 8) # Lower than open rate trade.open_date = five_hours_ago - assert trade.calc_profit(rate=0.00000437, interest_rate=0.00025) == 0.01027826 - # == 0.010278258156911545 - assert trade.calc_profit_ratio(rate=0.00000437, interest_rate=0.00025) == 8.49860384 - # == 8.498603842864217 + assert trade.calc_profit(rate=0.00000437, interest_rate=0.00025) == round(0.01027826, 8) + assert trade.calc_profit_ratio( + rate=0.00000437, interest_rate=0.00025) == round(2.677453699564163, 8) # Custom closing rate and custom fee rate # Higher than open rate - assert trade.calc_profit(rate=0.00004374, fee=0.003, interest_rate=0.0005) == -0.00062728 - # == -0.0006272823636897188 - assert trade.calc_profit_ratio(rate=0.00004374, fee=0.003, interest_rate=0.0005) == -0.05177756 - # == -0.05177756162244562 + assert trade.calc_profit(rate=0.00004374, fee=0.003, + interest_rate=0.0005) == round(-0.00062728, 8) + assert trade.calc_profit_ratio(rate=0.00004374, fee=0.003, + interest_rate=0.0005) == round(-0.16340506919482353, 8) # Lower than open rate trade.open_date = ten_minutes_ago - assert trade.calc_profit(rate=0.00000437, fee=0.003, interest_rate=0.00025) == 0.01027773 - # == 0.010277730564070022 - assert trade.calc_profit_ratio(rate=0.00000437, fee=0.003, interest_rate=0.00025) == 8.49446196 - # == 8.494461964724694 + assert trade.calc_profit(rate=0.00000437, fee=0.003, + interest_rate=0.00025) == round(0.01027773, 8) + assert trade.calc_profit_ratio(rate=0.00000437, fee=0.003, + interest_rate=0.00025) == round(2.677316263299785, 8) # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 trade.update(market_exit_short_order) - assert trade.calc_profit() == 0.00014148 - # == 0.00014147984366976937 - assert trade.calc_profit_ratio() == 0.01246938 - # == 0.012469377026284034 + assert trade.calc_profit() == round(0.00014148, 8) + assert trade.calc_profit_ratio() == round(0.03685505, 8) # Test with a custom fee rate on the close trade # assert trade.calc_profit(fee=0.003) == 0.00006163 From 25ff7269215931d27d2fe085bbf16ab99734dd8e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 2 Jul 2021 02:48:30 -0600 Subject: [PATCH 16/46] Wrote all tests for shorting --- freqtrade/persistence/models.py | 3 - tests/conftest.py | 134 ++++++- tests/conftest_trades.py | 102 ++++-- tests/test_persistence_long.py | 616 ++++++++++++++++++++++++++++++++ tests/test_persistence_short.py | 131 ++----- 5 files changed, 852 insertions(+), 134 deletions(-) create mode 100644 tests/test_persistence_long.py diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index ec5c15cee..a974691be 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -132,9 +132,6 @@ class Order(_DECL_BASE): order_filled_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True) - leverage = Column(Float, nullable=True, default=None) - is_short = Column(Boolean, nullable=True, default=False) - def __repr__(self): return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' f'side={self.side}, order_type={self.order_type}, status={self.status})') diff --git a/tests/conftest.py b/tests/conftest.py index 3c071f2f3..b17f9658e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -204,11 +204,34 @@ def create_mock_trades(fee, use_db: bool = True): add_trade(trade) trade = mock_trade_6(fee) add_trade(trade) - # TODO: margin trades - # trade = short_trade(fee) - # add_trade(trade) - # trade = leverage_trade(fee) - # add_trade(trade) + + +def create_mock_trades_with_leverage(fee, use_db: bool = True): + """ + Create some fake trades ... + """ + def add_trade(trade): + if use_db: + Trade.query.session.add(trade) + else: + LocalTrade.add_bt_trade(trade) + # Simulate dry_run entries + trade = mock_trade_1(fee) + add_trade(trade) + trade = mock_trade_2(fee) + add_trade(trade) + trade = mock_trade_3(fee) + add_trade(trade) + trade = mock_trade_4(fee) + add_trade(trade) + trade = mock_trade_5(fee) + add_trade(trade) + trade = mock_trade_6(fee) + add_trade(trade) + trade = short_trade(fee) + add_trade(trade) + trade = leverage_trade(fee) + add_trade(trade) if use_db: Trade.query.session.flush() @@ -2094,7 +2117,7 @@ def limit_short_order_open(): 'cost': 0.00106733393, 'remaining': 90.99181073, 'status': 'open', - 'is_short': True + 'exchange': 'binance' } @@ -2111,7 +2134,8 @@ def limit_exit_short_order_open(): 'amount': 90.99181073, 'filled': 0.0, 'remaining': 90.99181073, - 'status': 'open' + 'status': 'open', + 'exchange': 'binance' } @@ -2147,7 +2171,8 @@ def market_short_order(): 'remaining': 0.0, 'status': 'closed', 'is_short': True, - 'leverage': 3.0 + # 'leverage': 3.0, + 'exchange': 'kraken' } @@ -2164,5 +2189,96 @@ def market_exit_short_order(): 'filled': 91.99181073, 'remaining': 0.0, 'status': 'closed', - 'leverage': 3.0 + # 'leverage': 3.0, + 'exchange': 'kraken' + } + + +# leverage 3x +@pytest.fixture(scope='function') +def limit_leveraged_buy_order_open(): + return { + 'id': 'mocked_limit_buy', + 'type': 'limit', + 'side': 'buy', + 'symbol': 'mocked', + 'datetime': arrow.utcnow().isoformat(), + 'timestamp': arrow.utcnow().int_timestamp, + 'price': 0.00001099, + 'amount': 272.97543219, + 'filled': 0.0, + 'cost': 0.0029999999997681, + 'remaining': 272.97543219, + 'status': 'open', + 'exchange': 'binance' + } + + +@pytest.fixture(scope='function') +def limit_leveraged_buy_order(limit_leveraged_buy_order_open): + order = deepcopy(limit_leveraged_buy_order_open) + order['status'] = 'closed' + order['filled'] = order['amount'] + order['remaining'] = 0.0 + return order + + +@pytest.fixture +def limit_leveraged_sell_order_open(): + return { + 'id': 'mocked_limit_sell', + 'type': 'limit', + 'side': 'sell', + 'pair': 'mocked', + 'datetime': arrow.utcnow().isoformat(), + 'timestamp': arrow.utcnow().int_timestamp, + 'price': 0.00001173, + 'amount': 272.97543219, + 'filled': 0.0, + 'remaining': 272.97543219, + 'status': 'open', + 'exchange': 'binance' + } + + +@pytest.fixture +def limit_leveraged_sell_order(limit_leveraged_sell_order_open): + order = deepcopy(limit_leveraged_sell_order_open) + order['remaining'] = 0.0 + order['filled'] = order['amount'] + order['status'] = 'closed' + return order + + +@pytest.fixture(scope='function') +def market_leveraged_buy_order(): + return { + 'id': 'mocked_market_buy', + 'type': 'market', + 'side': 'buy', + 'symbol': 'mocked', + 'datetime': arrow.utcnow().isoformat(), + 'price': 0.00004099, + 'amount': 275.97543219, + 'filled': 275.97543219, + 'remaining': 0.0, + 'status': 'closed', + 'exchange': 'kraken' + } + + +@pytest.fixture +def market_leveraged_sell_order(): + return { + 'id': 'mocked_limit_sell', + 'type': 'market', + 'side': 'sell', + 'symbol': 'mocked', + 'datetime': arrow.utcnow().isoformat(), + 'price': 0.00004173, + 'amount': 275.97543219, + 'filled': 275.97543219, + 'remaining': 0.0, + 'status': 'closed', + 'exchange': 'kraken' } diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 41213732a..bc728dd44 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -310,7 +310,7 @@ def mock_trade_6(fee): def short_order(): return { - 'id': '1235', + 'id': '1236', 'symbol': 'ETC/BTC', 'status': 'closed', 'side': 'sell', @@ -319,14 +319,12 @@ def short_order(): 'amount': 123.0, 'filled': 123.0, 'remaining': 0.0, - 'leverage': 5.0, - 'isShort': True } def exit_short_order(): return { - 'id': '12366', + 'id': '12367', 'symbol': 'ETC/BTC', 'status': 'closed', 'side': 'buy', @@ -335,36 +333,60 @@ def exit_short_order(): 'amount': 123.0, 'filled': 123.0, 'remaining': 0.0, - 'leverage': 5.0, - 'isShort': True } def short_trade(fee): """ - Closed trade... + 10 minute short limit trade on binance + + Short trade + fee: 0.25% base + interest_rate: 0.05% per day + open_rate: 0.123 base + close_rate: 0.128 base + amount: 123.0 crypto + stake_amount: 15.129 base + borrowed: 123.0 crypto + time-periods: 10 minutes(rounds up to 1/24 time-period of 1 day) + interest: borrowed * interest_rate * time-periods + = 123.0 * 0.0005 * 1/24 = 0.0025625 crypto + open_value: (amount * open_rate) - (amount * open_rate * fee) + = (123 * 0.123) - (123 * 0.123 * 0.0025) + = 15.091177499999999 + amount_closed: amount + interest = 123 + 0.0025625 = 123.0025625 + close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) + = (123.0025625 * 0.128) + (123.0025625 * 0.128 * 0.0025) + = 15.78368882 + total_profit = open_value - close_value + = 15.091177499999999 - 15.78368882 + = -0.6925113200000013 + total_profit_percentage = total_profit / stake_amount + = -0.6925113200000013 / 15.129 + = -0.04577376693766946 + """ trade = Trade( pair='ETC/BTC', - stake_amount=0.001, + stake_amount=15.129, amount=123.0, amount_requested=123.0, fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, - close_rate=0.128, - close_profit=0.025, - close_profit_abs=0.000584127, + # close_rate=0.128, + # close_profit=-0.04577376693766946, + # close_profit_abs=-0.6925113200000013, exchange='binance', - is_open=False, + is_open=True, open_order_id='dry_run_exit_short_12345', strategy='DefaultStrategy', timeframe=5, sell_reason='sell_signal', # TODO-mg: Update to exit/close reason open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), - close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2), + # close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2), # borrowed= - isShort=True + is_short=True ) o = Order.parse_from_ccxt_object(short_order(), 'ETC/BTC', 'sell') trade.orders.append(o) @@ -375,7 +397,7 @@ def short_trade(fee): def leverage_order(): return { - 'id': '1235', + 'id': '1237', 'symbol': 'ETC/BTC', 'status': 'closed', 'side': 'buy', @@ -390,7 +412,7 @@ def leverage_order(): def leverage_order_sell(): return { - 'id': '12366', + 'id': '12368', 'symbol': 'ETC/BTC', 'status': 'closed', 'side': 'sell', @@ -399,34 +421,60 @@ def leverage_order_sell(): 'amount': 123.0, 'filled': 123.0, 'remaining': 0.0, - 'leverage': 5.0, - 'isShort': True } def leverage_trade(fee): """ - Closed trade... + 5 hour short limit trade on kraken + + Short trade + fee: 0.25% base + interest_rate: 0.05% per day + open_rate: 0.123 base + close_rate: 0.128 base + amount: 123.0 crypto + amount_with_leverage: 615.0 + stake_amount: 15.129 base + borrowed: 60.516 base + leverage: 5 + time-periods: 5 hrs( 5/4 time-period of 4 hours) + interest: borrowed * interest_rate * time-periods + = 60.516 * 0.0005 * 1/24 = 0.0378225 base + open_value: (amount * open_rate) - (amount * open_rate * fee) + = (615.0 * 0.123) - (615.0 * 0.123 * 0.0025) + = 75.4558875 + + close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) + = (615.0 * 0.128) + (615.0 * 0.128 * 0.0025) + = 78.9168 + total_profit = close_value - open_value - interest + = 78.9168 - 75.4558875 - 0.0378225 + = 3.423089999999992 + total_profit_percentage = total_profit / stake_amount + = 3.423089999999992 / 15.129 + = 0.22626016260162551 """ trade = Trade( pair='ETC/BTC', - stake_amount=0.001, - amount=615.0, - amount_requested=615.0, + stake_amount=15.129, + amount=123.0, + leverage=5, + amount_requested=123.0, fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, close_rate=0.128, - close_profit=0.005, # TODO-mg: Would this be -0.005 or -0.025 - close_profit_abs=0.000584127, - exchange='binance', + close_profit=0.22626016260162551, + close_profit_abs=3.423089999999992, + exchange='kraken', is_open=False, open_order_id='dry_run_leverage_sell_12345', strategy='DefaultStrategy', timeframe=5, sell_reason='sell_signal', # TODO-mg: Update to exit/close reason - open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), - close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2), + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=300), + close_date=datetime.now(tz=timezone.utc), # borrowed= ) o = Order.parse_from_ccxt_object(leverage_order(), 'ETC/BTC', 'sell') diff --git a/tests/test_persistence_long.py b/tests/test_persistence_long.py new file mode 100644 index 000000000..cd0267cd1 --- /dev/null +++ b/tests/test_persistence_long.py @@ -0,0 +1,616 @@ +import logging +from datetime import datetime, timedelta, timezone +from pathlib import Path +from types import FunctionType +from unittest.mock import MagicMock +import arrow +import pytest +from math import isclose +from sqlalchemy import create_engine, inspect, text +from freqtrade import constants +from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db +from tests.conftest import create_mock_trades_with_leverage, log_has, log_has_re + + +@pytest.mark.usefixtures("init_persistence") +def test_update_with_binance(limit_leveraged_buy_order, limit_leveraged_sell_order, fee, ten_minutes_ago, caplog): + """ + 10 minute leveraged limit trade on binance at 3x leverage + + Leveraged trade + fee: 0.25% base + interest_rate: 0.05% per day + open_rate: 0.00001099 base + close_rate: 0.00001173 base + amount: 272.97543219 crypto + stake_amount: 0.0009999999999226999 base + borrowed: 0.0019999999998453998 base + time-periods: 10 minutes(rounds up to 1/24 time-period of 1 day) + interest: borrowed * interest_rate * time-periods + = 0.0019999999998453998 * 0.0005 * 1/24 = 4.166666666344583e-08 base + open_value: (amount * open_rate) + (amount * open_rate * fee) + = (272.97543219 * 0.00001099) + (272.97543219 * 0.00001099 * 0.0025) + = 0.0030074999997675204 + close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) + = (272.97543219 * 0.00001173) - (272.97543219 * 0.00001173 * 0.0025) + = 0.003193996815039728 + total_profit = close_value - open_value - interest + = 0.003193996815039728 - 0.0030074999997675204 - 4.166666666344583e-08 + = 0.00018645514860554435 + total_profit_percentage = total_profit / stake_amount + = 0.00018645514860554435 / 0.0009999999999226999 + = 0.18645514861995735 + + """ + trade = Trade( + id=2, + pair='ETH/BTC', + stake_amount=0.0009999999999226999, + open_rate=0.01, + amount=5, + is_open=True, + open_date=ten_minutes_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + # borrowed=90.99181073, + interest_rate=0.0005, + exchange='binance' + ) + # assert trade.open_order_id is None + assert trade.close_profit is None + assert trade.close_date is None + assert trade.borrowed is None + assert trade.is_short is None + # trade.open_order_id = 'something' + trade.update(limit_leveraged_buy_order) + # assert trade.open_order_id is None + assert trade.open_rate == 0.00001099 + assert trade.close_profit is None + assert trade.close_date is None + assert trade.borrowed == 0.0019999999998453998 + assert trade.is_short is True + assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, " + r"pair=ETH/BTC, amount=272.97543219, open_rate=0.00001099, open_since=.*\).", + caplog) + caplog.clear() + # trade.open_order_id = 'something' + trade.update(limit_leveraged_sell_order) + # assert trade.open_order_id is None + assert trade.close_rate == 0.00001173 + assert trade.close_profit == 0.18645514861995735 + assert trade.close_date is not None + assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, " + r"pair=ETH/BTC, amount=272.97543219, open_rate=0.00001099, open_since=.*\).", + caplog) + + +@pytest.mark.usefixtures("init_persistence") +def test_update_market_order(limit_leveraged_buy_order, limit_leveraged_sell_order, fee, ten_minutes_ago, caplog): + """ + 10 minute leveraged market trade on Kraken at 3x leverage + Short trade + fee: 0.25% base + interest_rate: 0.05% per 4 hrs + open_rate: 0.00004099 base + close_rate: 0.00004173 base + amount: 91.99181073 * leverage(3) = 275.97543219 crypto + stake_amount: 0.0037707443218227 + borrowed: 0.0075414886436454 base + time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) + interest: borrowed * interest_rate * time-periods + = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto + open_value: (amount * open_rate) + (amount * open_rate * fee) + = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) + = 0.01134051354788177 + close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) + = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.0025) + = 0.011487663648325479 + total_profit = close_value - open_value - interest + = 0.011487663648325479 - 0.01134051354788177 - 3.7707443218227e-06 + = 0.0001433793561218866 + total_profit_percentage = total_profit / stake_amount + = 0.0001433793561218866 / 0.0037707443218227 + = 0.03802415223225211 + """ + trade = Trade( + id=1, + pair='ETH/BTC', + stake_amount=0.0037707443218227, + amount=5, + open_rate=0.01, + is_open=True, + leverage=3, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_date=ten_minutes_ago, + interest_rate=0.0005, + exchange='kraken' + ) + trade.open_order_id = 'something' + trade.update(limit_leveraged_buy_order) + assert trade.leverage == 3.0 + assert trade.is_short == True + assert trade.open_order_id is None + assert trade.open_rate == 0.00004099 + assert trade.close_profit is None + assert trade.close_date is None + assert trade.interest_rate == 0.0005 + # TODO: Uncomment the next assert and make it work. + # The logger also has the exact same but there's some spacing in there + assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, " + r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004099, open_since=.*\).", + caplog) + caplog.clear() + trade.is_open = True + trade.open_order_id = 'something' + trade.update(limit_leveraged_sell_order) + assert trade.open_order_id is None + assert trade.close_rate == 0.00004173 + assert trade.close_profit == 0.03802415223225211 + assert trade.close_date is not None + # TODO: The amount should maybe be the opening amount + the interest + # TODO: Uncomment the next assert and make it work. + # The logger also has the exact same but there's some spacing in there + assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " + r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004099, open_since=.*\).", + caplog) + + +@pytest.mark.usefixtures("init_persistence") +def test_calc_open_close_trade_price(limit_leveraged_buy_order, limit_leveraged_sell_order, five_hours_ago, fee): + """ + 5 hour leveraged trade on Binance + + fee: 0.25% base + interest_rate: 0.05% per day + open_rate: 0.00001099 base + close_rate: 0.00001173 base + amount: 272.97543219 crypto + stake_amount: 0.0009999999999226999 base + borrowed: 0.0019999999998453998 base + time-periods: 5 hours(rounds up to 5/24 time-period of 1 day) + interest: borrowed * interest_rate * time-periods + = 0.0019999999998453998 * 0.0005 * 5/24 = 2.0833333331722917e-07 base + open_value: (amount * open_rate) + (amount * open_rate * fee) + = (272.97543219 * 0.00001099) + (272.97543219 * 0.00001099 * 0.0025) + = 0.0030074999997675204 + close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) + = (272.97543219 * 0.00001173) - (272.97543219 * 0.00001173 * 0.0025) + = 0.003193996815039728 + total_profit = close_value - open_value - interest + = 0.003193996815039728 - 0.0030074999997675204 - 2.0833333331722917e-07 + = 0.00018628848193889054 + total_profit_percentage = total_profit / stake_amount + = 0.00018628848193889054 / 0.0009999999999226999 + = 0.18628848195329067 + """ + trade = Trade( + pair='ETH/BTC', + stake_amount=0.0009999999999226999, + open_rate=0.01, + amount=5, + open_date=five_hours_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + interest_rate=0.0005 + ) + trade.open_order_id = 'something' + trade.update(limit_leveraged_buy_order) + assert trade._calc_open_trade_value() == 0.0030074999997675204 + trade.update(limit_leveraged_sell_order) + + # Will be slightly different due to slight changes in compilation time, and the fact that interest depends on time + assert round(trade.calc_close_trade_value(), 11) == round(0.003193996815039728, 11) + # Profit in BTC + assert round(trade.calc_profit(), 8) == round(0.18628848195329067, 8) + # Profit in percent + # assert round(trade.calc_profit_ratio(), 11) == round(0.05822425142973869, 11) + + +@pytest.mark.usefixtures("init_persistence") +def test_trade_close(fee, five_hours_ago): + """ + 5 hour leveraged market trade on Kraken at 3x leverage + fee: 0.25% base + interest_rate: 0.05% per 4 hrs + open_rate: 0.1 base + close_rate: 0.2 base + amount: 5 * leverage(3) = 15 crypto + stake_amount: 0.5 + borrowed: 1 base + time-periods: 5/4 periods of 4hrs + interest: borrowed * interest_rate * time-periods + = 1 * 0.0005 * 5/4 = 0.000625 crypto + open_value: (amount * open_rate) + (amount * open_rate * fee) + = (15 * 0.1) + (15 * 0.1 * 0.0025) + = 1.50375 + close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) + = (15 * 0.2) - (15 * 0.2 * 0.0025) + = 2.9925 + total_profit = close_value - open_value - interest + = 2.9925 - 1.50375 - 0.000625 + = 1.4881250000000001 + total_profit_percentage = total_profit / stake_amount + = 1.4881250000000001 / 0.5 + = 2.9762500000000003 + """ + trade = Trade( + pair='ETH/BTC', + stake_amount=0.1, + open_rate=0.01, + amount=5, + is_open=True, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_date=five_hours_ago, + exchange='kraken', + leverage=3.0, + interest_rate=0.0005 + ) + assert trade.close_profit is None + assert trade.close_date is None + assert trade.is_open is True + trade.close(0.02) + assert trade.is_open is False + assert trade.close_profit == round(2.9762500000000003, 8) + assert trade.close_date is not None + + # TODO-mg: Remove these comments probably + # new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, + # assert trade.close_date != new_date + # # Close should NOT update close_date if the trade has been closed already + # assert trade.is_open is False + # trade.close_date = new_date + # trade.close(0.02) + # assert trade.close_date == new_date + + +@pytest.mark.usefixtures("init_persistence") +def test_calc_close_trade_price_exception(limit_leveraged_buy_order, fee): + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + open_rate=0.1, + amount=5, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + interest_rate=0.0005, + borrowed=0.002 + ) + trade.open_order_id = 'something' + trade.update(limit_leveraged_buy_order) + assert trade.calc_close_trade_value() == 0.0 + + +@pytest.mark.usefixtures("init_persistence") +def test_update_open_order(limit_leveraged_buy_order): + trade = Trade( + pair='ETH/BTC', + stake_amount=1.00, + open_rate=0.01, + amount=5, + fee_open=0.1, + fee_close=0.1, + interest_rate=0.0005, + borrowed=2.00, + exchange='binance', + ) + assert trade.open_order_id is None + assert trade.close_profit is None + assert trade.close_date is None + limit_leveraged_buy_order['status'] = 'open' + trade.update(limit_leveraged_buy_order) + assert trade.open_order_id is None + assert trade.close_profit is None + assert trade.close_date is None + + +@pytest.mark.usefixtures("init_persistence") +def test_calc_open_trade_value(market_leveraged_buy_order, ten_minutes_ago, fee): + """ + 10 minute leveraged market trade on Kraken at 3x leverage + Short trade + fee: 0.25% base + interest_rate: 0.05% per 4 hrs + open_rate: 0.00004099 base + close_rate: 0.00004173 base + amount: 91.99181073 * leverage(3) = 275.97543219 crypto + stake_amount: 0.0037707443218227 + borrowed: 0.0075414886436454 base + time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) + interest: borrowed * interest_rate * time-periods + = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto + open_value: (amount * open_rate) + (amount * open_rate * fee) + = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) + = 0.01134051354788177 + """ + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + open_rate=0.00004099, + open_date=ten_minutes_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + interest_rate=0.0005, + exchange='kraken', + leverage=3 + ) + trade.open_order_id = 'open_trade' + trade.update(market_leveraged_buy_order) # Buy @ 0.00001099 + # Get the open rate price with the standard fee rate + assert trade._calc_open_trade_value() == 0.01134051354788177 + trade.fee_open = 0.003 + # Get the open rate price with a custom fee rate + assert trade._calc_open_trade_value() == 0.011346169664364504 + + +@pytest.mark.usefixtures("init_persistence") +def test_calc_close_trade_price(market_leveraged_buy_order, market_leveraged_sell_order, ten_minutes_ago, fee): + """ + 10 minute leveraged market trade on Kraken at 3x leverage + Short trade + fee: 0.25% base + interest_rate: 0.05% per 4 hrs + open_rate: 0.00004099 base + close_rate: 0.00004173 base + amount: 91.99181073 * leverage(3) = 275.97543219 crypto + stake_amount: 0.0037707443218227 + borrowed: 0.0075414886436454 base + time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) + interest: borrowed * interest_rate * time-periods + = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto + open_value: (amount * open_rate) + (amount * open_rate * fee) + = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) + = 0.01134051354788177 + close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) + = (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.0025) = 0.0033970229911415386 + = (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.003) = 0.0033953202227249265 + = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.005) = 0.011458872511362258 + + """ + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + open_rate=0.00001099, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_date=ten_minutes_ago, + interest_rate=0.0005, + is_short=True, + leverage=3.0, + exchange='kraken', + ) + trade.open_order_id = 'close_trade' + trade.update(market_leveraged_buy_order) # Buy @ 0.00001099 + # Get the close rate price with a custom close rate and a regular fee rate + assert isclose(trade.calc_close_trade_value(rate=0.00001234), 0.0033970229911415386) + # Get the close rate price with a custom close rate and a custom fee rate + assert isclose(trade.calc_close_trade_value(rate=0.00001234, fee=0.003), 0.0033953202227249265) + # Test when we apply a Sell order, and ask price with a custom fee rate + trade.update(market_leveraged_sell_order) + assert isclose(trade.calc_close_trade_value(fee=0.005), 0.011458872511362258) + + +@pytest.mark.usefixtures("init_persistence") +def test_calc_profit(market_leveraged_buy_order, market_leveraged_sell_order, ten_minutes_ago, five_hours_ago, fee): + """ + # TODO: Update this one + Leveraged trade on Kraken at 3x leverage + fee: 0.25% base or 0.3% + interest_rate: 0.05%, 0.25% per 4 hrs + open_rate: 0.00004099 base + close_rate: 0.00004173 base + stake_amount: 0.0037707443218227 + amount: 91.99181073 * leverage(3) = 275.97543219 crypto + borrowed: 0.0075414886436454 base + time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) + 5 hours = 5/4 + + interest: borrowed * interest_rate * time-periods + = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto + = 0.0075414886436454 * 0.00025 * 5/4 = 2.3567152011391876e-06 crypto + = 0.0075414886436454 * 0.0005 * 5/4 = 4.713430402278375e-06 crypto + = 0.0075414886436454 * 0.00025 * 1 = 1.88537216091135e-06 crypto + open_value: (amount * open_rate) + (amount * open_rate * fee) + = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) = 0.01134051354788177 + close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) + (275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.0025) = 0.014793842426575873 + (275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.0025) = 0.0012029976070736241 + (275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.003) = 0.014786426966712927 + (275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.003) = 0.0012023946007542888 + total_profit = close_value - open_value + = 0.014793842426575873 - 0.01134051354788177 = 0.003453328878694104 + = 0.0012029976070736241 - 0.01134051354788177 = -0.010137515940808145 + = 0.014786426966712927 - 0.01134051354788177 = 0.0034459134188311574 + = 0.0012023946007542888 - 0.01134051354788177 = -0.01013811894712748 + total_profit_percentage = total_profit / stake_amount + 0.003453328878694104/0.0037707443218227 = 0.9158215418394733 + -0.010137515940808145/0.0037707443218227 = -2.6884654793852154 + 0.0034459134188311574/0.0037707443218227 = 0.9138549646255183 + -0.01013811894712748/0.0037707443218227 = -2.6886253964381557 + + """ + trade = Trade( + pair='ETH/BTC', + stake_amount=0.0038388182617629, + amount=5, + open_rate=0.00004099, + open_date=ten_minutes_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='kraken', + leverage=3.0, + interest_rate=0.0005 + ) + trade.open_order_id = 'something' + trade.update(market_leveraged_buy_order) # Buy @ 0.00001099 + # Custom closing rate and regular fee rate + + # Higher than open rate + assert trade.calc_profit(rate=0.00004374, interest_rate=0.0005) == round( + 0.003453328878694104, 8) + assert trade.calc_profit_ratio( + rate=0.00004374, interest_rate=0.0005) == round(0.9158215418394733, 8) + + # Lower than open rate + trade.open_date = five_hours_ago + assert trade.calc_profit( + rate=0.00000437, interest_rate=0.00025) == round(-0.010137515940808145, 8) + assert trade.calc_profit_ratio( + rate=0.00000437, interest_rate=0.00025) == round(-2.6884654793852154, 8) + + # Custom closing rate and custom fee rate + # Higher than open rate + assert trade.calc_profit(rate=0.00004374, fee=0.003, + interest_rate=0.0005) == round(0.0034459134188311574, 8) + assert trade.calc_profit_ratio(rate=0.00004374, fee=0.003, + interest_rate=0.0005) == round(0.9138549646255183, 8) + + # Lower than open rate + trade.open_date = ten_minutes_ago + assert trade.calc_profit(rate=0.00000437, fee=0.003, + interest_rate=0.00025) == round(-0.01013811894712748, 8) + assert trade.calc_profit_ratio(rate=0.00000437, fee=0.003, + interest_rate=0.00025) == round(-2.6886253964381557, 8) + + # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 + trade.update(market_leveraged_sell_order) + assert trade.calc_profit() == round(0.0001433793561218866, 8) + assert trade.calc_profit_ratio() == round(0.03802415223225211, 8) + + # Test with a custom fee rate on the close trade + # assert trade.calc_profit(fee=0.003) == 0.00006163 + # assert trade.calc_profit_ratio(fee=0.003) == 0.06147824 + + +@pytest.mark.usefixtures("init_persistence") +def test_interest_kraken(market_leveraged_buy_order, ten_minutes_ago, five_hours_ago, fee): + """ + Market trade on Kraken at 3x and 8x leverage + Short trade + interest_rate: 0.05%, 0.25% per 4 hrs + open_rate: 0.00004099 base + close_rate: 0.00004173 base + stake_amount: 0.0037707443218227 + borrowed: 0.0075414886436454 + amount: + 91.99181073 * leverage(3) = 275.97543219 crypto + 91.99181073 * leverage(5) = 459.95905365 crypto + borrowed: + 0.0075414886436454 base + 0.0150829772872908 base + time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) + 5 hours = 5/4 + + interest: borrowed * interest_rate * time-periods + = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 base + = 0.0075414886436454 * 0.00025 * 5/4 = 2.3567152011391876e-06 base + = 0.0150829772872908 * 0.0005 * 5/4 = 9.42686080455675e-06 base + = 0.0150829772872908 * 0.00025 * 1 = 3.7707443218227e-06 base + """ + + trade = Trade( + pair='ETH/BTC', + stake_amount=0.0037707443218227, + amount=91.99181073, + open_rate=0.00001099, + open_date=ten_minutes_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='kraken', + leverage=3.0, + interest_rate=0.0005 + ) + + assert float(round(trade.calculate_interest(), 8)) == 3.7707443218227e-06 + trade.open_date = five_hours_ago + assert float(round(trade.calculate_interest(interest_rate=0.00025), 8) + ) == 2.3567152011391876e-06 # TODO: Fails with 0.08624233 + + trade = Trade( + pair='ETH/BTC', + stake_amount=0.0037707443218227, + amount=91.99181073, + open_rate=0.00001099, + open_date=five_hours_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='kraken', + is_short=True, + leverage=5.0, + interest_rate=0.0005 + ) + + assert float(round(trade.calculate_interest(), 8) + ) == 9.42686080455675e-06 # TODO: Fails with 0.28747445 + trade.open_date = ten_minutes_ago + assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)) == 3.7707443218227e-06 + + +@pytest.mark.usefixtures("init_persistence") +def test_interest_binance(market_leveraged_buy_order, ten_minutes_ago, five_hours_ago, fee): + """ + Market trade on Kraken at 3x and 8x leverage + Short trade + interest_rate: 0.05%, 0.25% per 4 hrs + open_rate: 0.00004099 base + close_rate: 0.00004173 base + stake_amount: 0.0037707443218227 + borrowed: 0.0075414886436454 + amount: + 91.99181073 * leverage(3) = 275.97543219 crypto + 91.99181073 * leverage(5) = 459.95905365 crypto + borrowed: + 0.0075414886436454 base + 0.0150829772872908 base + time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) + 5 hours = 5/24 + + interest: borrowed * interest_rate * time-periods + = 0.0075414886436454 * 0.0005 * 1/24 = 1.571143467426125e-07 base + = 0.0075414886436454 * 0.00025 * 5/24 = 3.9278586685653125e-07 base + = 0.0150829772872908 * 0.0005 * 5/24 = 1.571143467426125e-06 base + = 0.0150829772872908 * 0.00025 * 1/24 = 1.571143467426125e-07 base + """ + + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=275.97543219, + open_rate=0.00001099, + open_date=ten_minutes_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + is_short=True, + borrowed=275.97543219, + interest_rate=0.0005 + ) + + assert float(round(trade.calculate_interest(), 8)) == 1.571143467426125e-07 + trade.open_date = five_hours_ago + assert float(round(trade.calculate_interest(interest_rate=0.00025), 8) + ) == 3.9278586685653125e-07 + + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=459.95905365, + open_rate=0.00001099, + open_date=five_hours_ago, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + is_short=True, + borrowed=459.95905365, + interest_rate=0.0005 + ) + + assert float(round(trade.calculate_interest(), 8)) == 1.571143467426125e-06 + trade.open_date = ten_minutes_ago + assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)) == 1.571143467426125e-07 diff --git a/tests/test_persistence_short.py b/tests/test_persistence_short.py index b240de006..759b25a1a 100644 --- a/tests/test_persistence_short.py +++ b/tests/test_persistence_short.py @@ -10,7 +10,7 @@ from sqlalchemy import create_engine, inspect, text from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db -from tests.conftest import create_mock_trades, log_has, log_has_re +from tests.conftest import create_mock_trades_with_leverage, log_has, log_has_re @pytest.mark.usefixtures("init_persistence") @@ -43,9 +43,6 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, ten = (0.0010646656050132426 - 0.0010025208853391716) / 0.0010673339398629 = 0.05822425142973869 - #Old - = 1-(0.0010025208853391716/0.0010646656050132426) - = 0.05837017687191848 """ trade = Trade( id=2, @@ -295,7 +292,7 @@ def test_calc_close_trade_price_exception(limit_short_order, fee): exchange='binance', interest_rate=0.0005, is_short=True, - leverage=3.0 + borrowed=15 ) trade.open_order_id = 'something' trade.update(limit_short_order) @@ -636,40 +633,41 @@ def test_adjust_stop_loss(fee): assert trade.initial_stop_loss_pct == 0.05 # Get percent of profit with a custom rate (Higher than open rate) trade.adjust_stop_loss(0.7, 0.1) - # assert round(trade.stop_loss, 8) == 1.17 #TODO-mg: What is this test? + assert round(trade.stop_loss, 8) == 1.17 # TODO-mg: What is this test? assert trade.stop_loss_pct == 0.1 assert trade.initial_stop_loss == 1.05 assert trade.initial_stop_loss_pct == 0.05 # current rate lower again ... should not change trade.adjust_stop_loss(0.8, -0.1) - # assert round(trade.stop_loss, 8) == 1.17 #TODO-mg: What is this test? + assert round(trade.stop_loss, 8) == 1.17 # TODO-mg: What is this test? assert trade.initial_stop_loss == 1.05 assert trade.initial_stop_loss_pct == 0.05 # current rate higher... should raise stoploss trade.adjust_stop_loss(0.6, -0.1) - # assert round(trade.stop_loss, 8) == 1.26 #TODO-mg: What is this test? + assert round(trade.stop_loss, 8) == 1.26 # TODO-mg: What is this test? assert trade.initial_stop_loss == 1.05 assert trade.initial_stop_loss_pct == 0.05 # Initial is true but stop_loss set - so doesn't do anything trade.adjust_stop_loss(0.3, -0.1, True) - # assert round(trade.stop_loss, 8) == 1.26 #TODO-mg: What is this test? + assert round(trade.stop_loss, 8) == 1.26 # TODO-mg: What is this test? assert trade.initial_stop_loss == 1.05 assert trade.initial_stop_loss_pct == 0.05 assert trade.stop_loss_pct == 0.1 # TODO-mg: Do a test with a trade that has a liquidation price -# TODO: I don't know how to do this test, but it should be tested for shorts -# @pytest.mark.usefixtures("init_persistence") -# @pytest.mark.parametrize('use_db', [True, False]) -# def test_get_open(fee, use_db): -# Trade.use_db = use_db -# Trade.reset_trades() -# create_mock_trades(fee, use_db) -# assert len(Trade.get_open_trades()) == 4 -# Trade.use_db = True + +@pytest.mark.usefixtures("init_persistence") +@pytest.mark.parametrize('use_db', [True, False]) +def test_get_open(fee, use_db): + Trade.use_db = use_db + Trade.reset_trades() + create_mock_trades_with_leverage(fee, use_db) + assert len(Trade.get_open_trades()) == 5 + Trade.use_db = True def test_stoploss_reinitialization(default_conf, fee): + # TODO-mg: I don't understand this at all, I was just going in the opposite direction as the matching function form test_persistance.py init_db(default_conf['db_url']) trade = Trade( pair='ETH/BTC', @@ -721,83 +719,26 @@ def test_stoploss_reinitialization(default_conf, fee): assert trade_adj.initial_stop_loss == 1.04 assert trade_adj.initial_stop_loss_pct == 0.04 -# @pytest.mark.usefixtures("init_persistence") -# @pytest.mark.parametrize('use_db', [True, False]) -# def test_total_open_trades_stakes(fee, use_db): -# Trade.use_db = use_db -# Trade.reset_trades() -# res = Trade.total_open_trades_stakes() -# assert res == 0 -# create_mock_trades(fee, use_db) -# res = Trade.total_open_trades_stakes() -# assert res == 0.004 -# Trade.use_db = True -# @pytest.mark.usefixtures("init_persistence") -# def test_get_overall_performance(fee): -# create_mock_trades(fee) -# res = Trade.get_overall_performance() -# assert len(res) == 2 -# assert 'pair' in res[0] -# assert 'profit' in res[0] -# assert 'count' in res[0] +@pytest.mark.usefixtures("init_persistence") +@pytest.mark.parametrize('use_db', [True, False]) +def test_total_open_trades_stakes(fee, use_db): + Trade.use_db = use_db + Trade.reset_trades() + res = Trade.total_open_trades_stakes() + assert res == 0 + create_mock_trades_with_leverage(fee, use_db) + res = Trade.total_open_trades_stakes() + assert res == 15.133 + Trade.use_db = True -# @pytest.mark.usefixtures("init_persistence") -# def test_get_best_pair(fee): -# res = Trade.get_best_pair() -# assert res is None -# create_mock_trades(fee) -# res = Trade.get_best_pair() -# assert len(res) == 2 -# assert res[0] == 'XRP/BTC' -# assert res[1] == 0.01 -# @pytest.mark.usefixtures("init_persistence") -# def test_update_order_from_ccxt(caplog): -# # Most basic order return (only has orderid) -# o = Order.parse_from_ccxt_object({'id': '1234'}, 'ETH/BTC', 'buy') -# assert isinstance(o, Order) -# assert o.ft_pair == 'ETH/BTC' -# assert o.ft_order_side == 'buy' -# assert o.order_id == '1234' -# assert o.ft_is_open -# ccxt_order = { -# 'id': '1234', -# 'side': 'buy', -# 'symbol': 'ETH/BTC', -# 'type': 'limit', -# 'price': 1234.5, -# 'amount': 20.0, -# 'filled': 9, -# 'remaining': 11, -# 'status': 'open', -# 'timestamp': 1599394315123 -# } -# o = Order.parse_from_ccxt_object(ccxt_order, 'ETH/BTC', 'buy') -# assert isinstance(o, Order) -# assert o.ft_pair == 'ETH/BTC' -# assert o.ft_order_side == 'buy' -# assert o.order_id == '1234' -# assert o.order_type == 'limit' -# assert o.price == 1234.5 -# assert o.filled == 9 -# assert o.remaining == 11 -# assert o.order_date is not None -# assert o.ft_is_open -# assert o.order_filled_date is None -# # Order has been closed -# ccxt_order.update({'filled': 20.0, 'remaining': 0.0, 'status': 'closed'}) -# o.update_from_ccxt_object(ccxt_order) -# assert o.filled == 20.0 -# assert o.remaining == 0.0 -# assert not o.ft_is_open -# assert o.order_filled_date is not None -# ccxt_order.update({'id': 'somethingelse'}) -# with pytest.raises(DependencyException, match=r"Order-id's don't match"): -# o.update_from_ccxt_object(ccxt_order) -# message = "aaaa is not a valid response object." -# assert not log_has(message, caplog) -# Order.update_orders([o], 'aaaa') -# assert log_has(message, caplog) -# # Call regular update - shouldn't fail. -# Order.update_orders([o], {'id': '1234'}) +@pytest.mark.usefixtures("init_persistence") +def test_get_best_pair(fee): + res = Trade.get_best_pair() + assert res is None + create_mock_trades_with_leverage(fee) + res = Trade.get_best_pair() + assert len(res) == 2 + assert res[0] == 'ETC/BTC' + assert res[1] == 0.22626016260162551 From 75b2c9ca1b205b8b5dd55af3867d83ec2261a086 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Jul 2021 17:03:12 +0200 Subject: [PATCH 17/46] Fix migrations, revert some parts related to amount properties --- freqtrade/persistence/migrations.py | 25 +++++---- freqtrade/persistence/models.py | 79 +++++++++++++++-------------- tests/test_persistence.py | 3 +- 3 files changed, 58 insertions(+), 49 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index ef4a5623b..efadc7467 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -91,7 +91,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stoploss_order_id, stoploss_last_update, max_rate, min_rate, sell_reason, sell_order_status, strategy, timeframe, open_trade_value, close_profit_abs, - leverage, borrowed, borrowed_currency, collateral_currency, interest_rate, liquidation_price, is_short + leverage, borrowed, borrowed_currency, collateral_currency, interest_rate, + liquidation_price, is_short ) select id, lower(exchange), case @@ -115,8 +116,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {sell_order_status} sell_order_status, {strategy} strategy, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, - {leverage} leverage, {borrowed} borrowed, {borrowed_currency} borrowed_currency, - {collateral_currency} collateral_currency, {interest_rate} interest_rate, + {leverage} leverage, {borrowed} borrowed, {borrowed_currency} borrowed_currency, + {collateral_currency} collateral_currency, {interest_rate} interest_rate, {liquidation_price} liquidation_price, {is_short} is_short from {table_back_name} """)) @@ -152,14 +153,17 @@ def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, col # let SQLAlchemy create the schema as required decl_base.metadata.create_all(engine) + leverage = get_column_def(cols, 'leverage', 'null') + is_short = get_column_def(cols, 'is_short', 'False') with engine.begin() as connection: connection.execute(text(f""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, average, remaining, cost, - order_date, order_filled_date, order_update_date, leverage) + order_date, order_filled_date, order_update_date, leverage, is_short) select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, null average, remaining, cost, - order_date, order_filled_date, order_update_date, leverage + order_date, order_filled_date, order_update_date, + {leverage} leverage, {is_short} is_short from {table_back_name} """)) @@ -174,8 +178,9 @@ def check_migrate(engine, decl_base, previous_tables) -> None: tabs = get_table_names_for_table(inspector, 'trades') table_back_name = get_backup_name(tabs, 'trades_bak') - # Check for latest column - if not has_column(cols, 'open_trade_value'): + # Last added column of trades table + # To determine if migrations need to run + if not has_column(cols, 'collateral_currency'): logger.info(f'Running database migration for trades - backup: {table_back_name}') migrate_trades_table(decl_base, inspector, engine, table_back_name, cols) # Reread columns - the above recreated the table! @@ -188,9 +193,11 @@ def check_migrate(engine, decl_base, previous_tables) -> None: else: cols_order = inspector.get_columns('orders') - if not has_column(cols_order, 'average'): + # Last added column of order table + # To determine if migrations need to run + if not has_column(cols_order, 'leverage'): tabs = get_table_names_for_table(inspector, 'orders') # Empty for now - as there is only one iteration of the orders table so far. table_back_name = get_backup_name(tabs, 'orders_bak') - migrate_orders_table(decl_base, inspector, engine, table_back_name, cols) + migrate_orders_table(decl_base, inspector, engine, table_back_name, cols_order) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a974691be..8a52b4d4e 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -234,7 +234,7 @@ class LocalTrade(): close_profit: Optional[float] = None close_profit_abs: Optional[float] = None stake_amount: float = 0.0 - _amount: float = 0.0 + amount: float = 0.0 amount_requested: Optional[float] = None open_date: datetime close_date: Optional[datetime] = None @@ -266,8 +266,8 @@ class LocalTrade(): interest_rate: float = 0.0 liquidation_price: float = None is_short: bool = False - _borrowed: float = 0.0 - _leverage: float = None # * You probably want to use LocalTrade.leverage instead + borrowed: float = 0.0 + leverage: float = None # @property # def base_currency(self) -> str: @@ -275,42 +275,45 @@ class LocalTrade(): # raise OperationalException('LocalTrade.pair must be assigned') # return self.pair.split("/")[1] - @property - def amount(self) -> float: - if self._leverage is not None: - return self._amount * self.leverage - else: - return self._amount + # TODO: @samgermain: Amount should be persisted "as is". + # I've partially reverted this (this killed most of your tests) + # but leave this here as i'm not sure where you intended to use this. + # @property + # def amount(self) -> float: + # if self._leverage is not None: + # return self._amount * self.leverage + # else: + # return self._amount - @amount.setter - def amount(self, value): - self._amount = value + # @amount.setter + # def amount(self, value): + # self._amount = value - @property - def borrowed(self) -> float: - if self._leverage is not None: - if self.is_short: - # If shorting the full amount must be borrowed - return self._amount * self._leverage - else: - # If not shorting, then the trader already owns a bit - return self._amount * (self._leverage-1) - else: - return self._borrowed + # @property + # def borrowed(self) -> float: + # if self._leverage is not None: + # if self.is_short: + # # If shorting the full amount must be borrowed + # return self._amount * self._leverage + # else: + # # If not shorting, then the trader already owns a bit + # return self._amount * (self._leverage-1) + # else: + # return self._borrowed - @borrowed.setter - def borrowed(self, value): - self._borrowed = value - self._leverage = None + # @borrowed.setter + # def borrowed(self, value): + # self._borrowed = value + # self._leverage = None - @property - def leverage(self) -> float: - return self._leverage + # @property + # def leverage(self) -> float: + # return self._leverage - @leverage.setter - def leverage(self, value): - self._leverage = value - self._borrowed = None + # @leverage.setter + # def leverage(self, value): + # self._leverage = value + # self._borrowed = None # End of margin trading properties @@ -639,7 +642,7 @@ class LocalTrade(): # sec_per_day = Decimal(86400) sec_per_hour = Decimal(3600) total_seconds = Decimal((now - open_date).total_seconds()) - #days = total_seconds/sec_per_day or zero + # days = total_seconds/sec_per_day or zero hours = total_seconds/sec_per_hour or zero rate = Decimal(interest_rate or self.interest_rate) @@ -877,7 +880,7 @@ class Trade(_DECL_BASE, LocalTrade): close_profit = Column(Float) close_profit_abs = Column(Float) stake_amount = Column(Float, nullable=False) - _amount = Column(Float) + amount = Column(Float) amount_requested = Column(Float) open_date = Column(DateTime, nullable=False, default=datetime.utcnow) close_date = Column(DateTime) @@ -904,8 +907,8 @@ class Trade(_DECL_BASE, LocalTrade): timeframe = Column(Integer, nullable=True) # Margin trading properties - _leverage: float = None # * You probably want to use LocalTrade.leverage instead - _borrowed = Column(Float, nullable=False, default=0.0) + leverage = Column(Float, nullable=True) # TODO: can this be nullable, or should it default to 1? (must also be changed in migrations eventually) + borrowed = Column(Float, nullable=False, default=0.0) interest_rate = Column(Float, nullable=False, default=0.0) liquidation_price = Column(Float, nullable=True) is_short = Column(Boolean, nullable=False, default=False) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 358b59243..484a8739a 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -723,13 +723,11 @@ def test_migrate_new(mocker, default_conf, fee, caplog): order_date DATETIME, order_filled_date DATETIME, order_update_date DATETIME, - leverage FLOAT, PRIMARY KEY (id), CONSTRAINT _order_pair_order_id UNIQUE (ft_pair, order_id), FOREIGN KEY(ft_trade_id) REFERENCES trades (id) ) """)) - # TODO-mg @xmatthias: Had to add field leverage to this table, check that this is correct connection.execute(text(""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, remaining, cost, order_date, @@ -752,6 +750,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert orders[1].order_id == 'stop_order_id222' assert orders[1].ft_order_side == 'stoploss' + assert orders[0].is_short is False def test_migrate_mid_state(mocker, default_conf, fee, caplog): From 9ddb6981dd1360e4ec9457472a894467e15ded6e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 4 Jul 2021 00:11:59 -0600 Subject: [PATCH 18/46] Updated tests to new persistence --- freqtrade/exchange/binance.py | 10 + freqtrade/exchange/kraken.py | 9 + freqtrade/persistence/migrations.py | 11 +- freqtrade/persistence/models.py | 118 ++--- tests/conftest.py | 90 ++-- tests/conftest_trades.py | 5 +- tests/rpc/test_rpc.py | 6 - tests/test_persistence.py | 64 +-- tests/test_persistence_long.py | 793 ++++++++++++++-------------- tests/test_persistence_short.py | 741 +++++++++++++------------- 10 files changed, 874 insertions(+), 973 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 0c470cb24..a8d60d6c0 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -3,6 +3,7 @@ import logging from typing import Dict import ccxt +from decimal import Decimal from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) @@ -89,3 +90,12 @@ class Binance(Exchange): f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e + + @staticmethod + def calculate_interest(borrowed: Decimal, hours: Decimal, interest_rate: Decimal) -> Decimal: + # Rate is per day but accrued hourly or something + # binance: https://www.binance.com/en-AU/support/faq/360030157812 + one = Decimal(1) + twenty_four = Decimal(24) + # TODO-mg: Is hours rounded? + return borrowed * interest_rate * max(hours, one)/twenty_four diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 1b069aa6c..2cd2ac118 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -3,6 +3,7 @@ import logging from typing import Any, Dict import ccxt +from decimal import Decimal from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) @@ -124,3 +125,11 @@ class Kraken(Exchange): f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e + + @staticmethod + def calculate_interest(borrowed: Decimal, hours: Decimal, interest_rate: Decimal) -> Decimal: + four = Decimal(4.0) + # https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading- + opening_fee = borrowed * interest_rate + roll_over_fee = borrowed * interest_rate * max(0, (hours-four)/four) + return opening_fee + roll_over_fee diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index efadc7467..8e2f708d5 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -49,9 +49,6 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col strategy = get_column_def(cols, 'strategy', 'null') leverage = get_column_def(cols, 'leverage', 'null') - borrowed = get_column_def(cols, 'borrowed', '0.0') - borrowed_currency = get_column_def(cols, 'borrowed_currency', 'null') - collateral_currency = get_column_def(cols, 'collateral_currency', 'null') interest_rate = get_column_def(cols, 'interest_rate', '0.0') liquidation_price = get_column_def(cols, 'liquidation_price', 'null') is_short = get_column_def(cols, 'is_short', 'False') @@ -91,8 +88,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stoploss_order_id, stoploss_last_update, max_rate, min_rate, sell_reason, sell_order_status, strategy, timeframe, open_trade_value, close_profit_abs, - leverage, borrowed, borrowed_currency, collateral_currency, interest_rate, - liquidation_price, is_short + leverage, interest_rate, liquidation_price, is_short ) select id, lower(exchange), case @@ -116,14 +112,11 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {sell_order_status} sell_order_status, {strategy} strategy, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, - {leverage} leverage, {borrowed} borrowed, {borrowed_currency} borrowed_currency, - {collateral_currency} collateral_currency, {interest_rate} interest_rate, + {leverage} leverage, {interest_rate} interest_rate, {liquidation_price} liquidation_price, {is_short} is_short from {table_back_name} """)) -# TODO: Does leverage go in here? - def migrate_open_orders_to_trades(engine): with engine.begin() as connection: diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8a52b4d4e..ebfae72b9 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -132,7 +132,11 @@ class Order(_DECL_BASE): order_filled_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True) + leverage = Column(Float, nullable=True, default=None) + is_short = Column(Boolean, nullable=True, default=False) + def __repr__(self): + return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' f'side={self.side}, order_type={self.order_type}, status={self.status})') @@ -226,7 +230,6 @@ class LocalTrade(): fee_close_currency: str = '' open_rate: float = 0.0 open_rate_requested: Optional[float] = None - # open_trade_value - calculated via _calc_open_trade_value open_trade_value: float = 0.0 close_rate: Optional[float] = None @@ -261,61 +264,23 @@ class LocalTrade(): timeframe: Optional[int] = None # Margin trading properties - borrowed_currency: str = None - collateral_currency: str = None interest_rate: float = 0.0 liquidation_price: float = None is_short: bool = False - borrowed: float = 0.0 leverage: float = None - # @property - # def base_currency(self) -> str: - # if not self.pair: - # raise OperationalException('LocalTrade.pair must be assigned') - # return self.pair.split("/")[1] + @property + def has_no_leverage(self) -> bool: + return (self.leverage == 1.0 and not self.is_short) or self.leverage is None - # TODO: @samgermain: Amount should be persisted "as is". - # I've partially reverted this (this killed most of your tests) - # but leave this here as i'm not sure where you intended to use this. - # @property - # def amount(self) -> float: - # if self._leverage is not None: - # return self._amount * self.leverage - # else: - # return self._amount - - # @amount.setter - # def amount(self, value): - # self._amount = value - - # @property - # def borrowed(self) -> float: - # if self._leverage is not None: - # if self.is_short: - # # If shorting the full amount must be borrowed - # return self._amount * self._leverage - # else: - # # If not shorting, then the trader already owns a bit - # return self._amount * (self._leverage-1) - # else: - # return self._borrowed - - # @borrowed.setter - # def borrowed(self, value): - # self._borrowed = value - # self._leverage = None - - # @property - # def leverage(self) -> float: - # return self._leverage - - # @leverage.setter - # def leverage(self, value): - # self._leverage = value - # self._borrowed = None - - # End of margin trading properties + @property + def borrowed(self) -> float: + if self.has_no_leverage: + return 0.0 + elif not self.is_short: + return self.stake_amount * (self.leverage-1) + else: + return self.amount @property def open_date_utc(self): @@ -326,13 +291,8 @@ class LocalTrade(): return self.close_date.replace(tzinfo=timezone.utc) def __init__(self, **kwargs): - if kwargs.get('leverage') and kwargs.get('borrowed'): - # TODO-mg: should I raise an error? - raise OperationalException('Cannot pass both borrowed and leverage to Trade') for key in kwargs: setattr(self, key, kwargs[key]) - if not self.is_short: - self.is_short = False self.recalc_open_trade_value() def __repr__(self): @@ -404,9 +364,6 @@ class LocalTrade(): 'max_rate': self.max_rate, 'leverage': self.leverage, - 'borrowed': self.borrowed, - 'borrowed_currency': self.borrowed_currency, - 'collateral_currency': self.collateral_currency, 'interest_rate': self.interest_rate, 'liquidation_price': self.liquidation_price, 'is_short': self.is_short, @@ -473,7 +430,7 @@ class LocalTrade(): # evaluate if the stop loss needs to be updated else: - # 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 + # stop losses only walk up, never down!, #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): logger.debug(f"{self.pair} - Adjusting stoploss...") self._set_new_stoploss(new_loss, stoploss) @@ -510,13 +467,8 @@ class LocalTrade(): """ order_type = order['type'] - if ('leverage' in order and 'borrowed' in order): - raise OperationalException( - 'Pass only one of Leverage or Borrowed to the order in update trade') - if 'is_short' in order and order['side'] == 'sell': # Only set's is_short on opening trades, ignores non-shorts - # TODO-mg: I don't like this, but it might be the only way self.is_short = order['is_short'] # Ignore open and cancelled orders @@ -527,15 +479,10 @@ class LocalTrade(): if order_type in ('market', 'limit') and self.is_opening_trade(order['side']): # Update open rate and actual amount - self.open_rate = float(safe_value_fallback(order, 'average', 'price')) self.amount = float(safe_value_fallback(order, 'filled', 'amount')) - - if 'borrowed' in order: - self.borrowed = order['borrowed'] - elif 'leverage' in order: + if 'leverage' in order: self.leverage = order['leverage'] - self.recalc_open_trade_value() if self.is_open: payment = "SELL" if self.is_short else "BUY" @@ -544,7 +491,8 @@ class LocalTrade(): elif order_type in ('market', 'limit') and self.is_closing_trade(order['side']): if self.is_open: payment = "BUY" if self.is_short else "SELL" - # TODO: On Shorts technically your buying a little bit more than the amount because it's the ammount plus the interest + # TODO-mg: On Shorts technically your buying a little bit more than the amount because it's the ammount plus the interest + # But this wll only print the original logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.') self.close(safe_value_fallback(order, 'average', 'price')) # TODO: Double check this elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): @@ -632,17 +580,16 @@ class LocalTrade(): : param interest_rate: interest_charge for borrowing this coin(optional). If interest_rate is not set self.interest_rate will be used """ - # TODO-mg: Need to set other conditions because sometimes self.open_date is not defined, but why would it ever not be set + zero = Decimal(0.0) - if not (self.borrowed): + # If nothing was borrowed + if (self.leverage == 1.0 and not self.is_short) or not self.leverage: return zero open_date = self.open_date.replace(tzinfo=None) - now = datetime.utcnow() - # sec_per_day = Decimal(86400) + now = (self.close_date or datetime.utcnow()).replace(tzinfo=None) sec_per_hour = Decimal(3600) total_seconds = Decimal((now - open_date).total_seconds()) - # days = total_seconds/sec_per_day or zero hours = total_seconds/sec_per_hour or zero rate = Decimal(interest_rate or self.interest_rate) @@ -654,7 +601,7 @@ class LocalTrade(): if self.exchange == 'binance': # Rate is per day but accrued hourly or something # binance: https://www.binance.com/en-AU/support/faq/360030157812 - return borrowed * rate * max(hours, one)/twenty_four # TODO-mg: Is hours rounded? + return borrowed * rate * max(hours, one)/twenty_four elif self.exchange == 'kraken': # https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading- opening_fee = borrowed * rate @@ -746,16 +693,15 @@ class LocalTrade(): if (self.is_short and close_trade_value == 0.0) or (not self.is_short and self.open_trade_value == 0.0): return 0.0 else: - if self.borrowed: # TODO: This is only needed so that previous tests that included dummy stake_amounts don't fail. Undate those tests and get rid of this else + if self.has_no_leverage: + # TODO: This is only needed so that previous tests that included dummy stake_amounts don't fail. Undate those tests and get rid of this else + profit_ratio = (close_trade_value/self.open_trade_value) - 1 + else: if self.is_short: profit_ratio = ((self.open_trade_value - close_trade_value) / self.stake_amount) else: profit_ratio = ((close_trade_value - self.open_trade_value) / self.stake_amount) - else: # TODO: This is only needed so that previous tests that included dummy stake_amounts don't fail. Undate those tests and get rid of this else - if self.is_short: - profit_ratio = 1 - (close_trade_value/self.open_trade_value) - else: - profit_ratio = (close_trade_value/self.open_trade_value) - 1 + return float(f"{profit_ratio:.8f}") def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: @@ -907,14 +853,10 @@ class Trade(_DECL_BASE, LocalTrade): timeframe = Column(Integer, nullable=True) # Margin trading properties - leverage = Column(Float, nullable=True) # TODO: can this be nullable, or should it default to 1? (must also be changed in migrations eventually) - borrowed = Column(Float, nullable=False, default=0.0) + leverage = Column(Float, nullable=True) interest_rate = Column(Float, nullable=False, default=0.0) liquidation_price = Column(Float, nullable=True) is_short = Column(Boolean, nullable=False, default=False) - # TODO: Bottom 2 might not be needed - borrowed_currency = Column(Float, nullable=True) - collateral_currency = Column(String(25), nullable=True) # End of margin trading properties def __init__(self, **kwargs): diff --git a/tests/conftest.py b/tests/conftest.py index b17f9658e..843769df0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,10 +7,12 @@ from datetime import datetime, timedelta from functools import reduce from pathlib import Path from unittest.mock import MagicMock, Mock, PropertyMock + import arrow import numpy as np import pytest from telegram import Chat, Message, Update + from freqtrade import constants from freqtrade.commands import Arguments from freqtrade.data.converter import ohlcv_to_dataframe @@ -23,7 +25,11 @@ from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, mock_trade_5, mock_trade_6, short_trade, leverage_trade) + + logging.getLogger('').setLevel(logging.INFO) + + # Do not mask numpy errors as warnings that no one read, raise the exсeption np.seterr(all='raise') @@ -63,6 +69,7 @@ def get_args(args): def get_mock_coro(return_value): async def mock_coro(*args, **kwargs): return return_value + return Mock(wraps=mock_coro) @@ -85,6 +92,7 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No if mock_markets: mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=get_markets())) + if api_mock: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) else: @@ -118,6 +126,7 @@ def patch_edge(mocker) -> None: # "LTC/BTC", # "XRP/BTC", # "NEO/BTC" + mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( return_value={ 'NEO/BTC': PairInfo(-0.20, 0.66, 3.71, 0.50, 1.71, 10, 25), @@ -131,6 +140,7 @@ def get_patched_edge(mocker, config) -> Edge: patch_edge(mocker) edge = Edge(config) return edge + # Functions for recurrent object patching @@ -191,6 +201,7 @@ def create_mock_trades(fee, use_db: bool = True): Trade.query.session.add(trade) else: LocalTrade.add_bt_trade(trade) + # Simulate dry_run entries trade = mock_trade_1(fee) add_trade(trade) @@ -220,14 +231,19 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True): add_trade(trade) trade = mock_trade_2(fee) add_trade(trade) + trade = mock_trade_3(fee) add_trade(trade) + trade = mock_trade_4(fee) add_trade(trade) + trade = mock_trade_5(fee) add_trade(trade) + trade = mock_trade_6(fee) add_trade(trade) + trade = short_trade(fee) add_trade(trade) trade = leverage_trade(fee) @@ -243,6 +259,7 @@ def patch_coingekko(mocker) -> None: :param mocker: mocker to patch coingekko class :return: None """ + tickermock = MagicMock(return_value={'bitcoin': {'usd': 12345.0}, 'ethereum': {'usd': 12345.0}}) listmock = MagicMock(return_value=[{'id': 'bitcoin', 'name': 'Bitcoin', 'symbol': 'btc', 'website_slug': 'bitcoin'}, @@ -253,13 +270,13 @@ def patch_coingekko(mocker) -> None: 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', get_price=tickermock, get_coins_list=listmock, + ) @pytest.fixture(scope='function') def init_persistence(default_conf): init_db(default_conf['db_url'], default_conf['dry_run']) - # TODO-mg: trade with leverage and/or borrowed? @pytest.fixture(scope="function") @@ -924,17 +941,18 @@ def limit_sell_order_old(): @pytest.fixture def limit_buy_order_old_partial(): - return {'id': 'mocked_limit_buy_old_partial', - 'type': 'limit', - 'side': 'buy', - 'symbol': 'ETH/BTC', - 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), - 'price': 0.00001099, - 'amount': 90.99181073, - 'filled': 23.0, - 'remaining': 67.99181073, - 'status': 'open' - } + return { + 'id': 'mocked_limit_buy_old_partial', + 'type': 'limit', + 'side': 'buy', + 'symbol': 'ETH/BTC', + 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), + 'price': 0.00001099, + 'amount': 90.99181073, + 'filled': 23.0, + 'remaining': 67.99181073, + 'status': 'open' + } @pytest.fixture @@ -950,6 +968,7 @@ def limit_buy_order_canceled_empty(request): # Indirect fixture # Documentation: # https://docs.pytest.org/en/latest/example/parametrize.html#apply-indirect-on-particular-arguments + exchange_name = request.param if exchange_name == 'ftx': return { @@ -1123,7 +1142,7 @@ def order_book_l2_usd(): [25.576, 262.016], [25.577, 178.557], [25.578, 78.614] - ], + ], 'timestamp': None, 'datetime': None, 'nonce': 2372149736 @@ -1739,6 +1758,7 @@ def edge_conf(default_conf): "max_trade_duration_minute": 1440, "remove_pumps": False } + return conf @@ -1776,7 +1796,6 @@ def rpc_balance(): 'used': 0.0 }, } - # TODO-mg: Add shorts and leverage? @pytest.fixture @@ -1796,9 +1815,12 @@ def import_fails() -> None: if name in ["filelock", 'systemd.journal', 'uvloop']: raise ImportError(f"No module named '{name}'") return realimport(name, *args, **kwargs) + builtins.__import__ = mockedimport + # Run test - then cleanup yield + # restore previous importfunction builtins.__import__ = realimport @@ -2083,6 +2105,7 @@ def saved_hyperopt_results(): 'is_best': False } ] + for res in hyperopt_res: res['results_metrics']['holding_avg_s'] = res['results_metrics']['holding_avg' ].total_seconds() @@ -2091,16 +2114,6 @@ def saved_hyperopt_results(): # * Margin Tests -@pytest.fixture -def ten_minutes_ago(): - return datetime.utcnow() - timedelta(hours=0, minutes=10) - - -@pytest.fixture -def five_hours_ago(): - return datetime.utcnow() - timedelta(hours=5, minutes=0) - - @pytest.fixture(scope='function') def limit_short_order_open(): return { @@ -2112,12 +2125,12 @@ def limit_short_order_open(): 'timestamp': arrow.utcnow().int_timestamp, 'price': 0.00001173, 'amount': 90.99181073, - 'borrowed': 90.99181073, + 'leverage': 1.0, 'filled': 0.0, 'cost': 0.00106733393, 'remaining': 90.99181073, 'status': 'open', - 'exchange': 'binance' + 'is_short': True } @@ -2131,11 +2144,10 @@ def limit_exit_short_order_open(): 'datetime': arrow.utcnow().isoformat(), 'timestamp': arrow.utcnow().int_timestamp, 'price': 0.00001099, - 'amount': 90.99181073, + 'amount': 90.99370639272354, 'filled': 0.0, - 'remaining': 90.99181073, - 'status': 'open', - 'exchange': 'binance' + 'remaining': 90.99370639272354, + 'status': 'open' } @@ -2166,13 +2178,12 @@ def market_short_order(): 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), 'price': 0.00004173, - 'amount': 91.99181073, - 'filled': 91.99181073, + 'amount': 275.97543219, + 'filled': 275.97543219, 'remaining': 0.0, 'status': 'closed', 'is_short': True, - # 'leverage': 3.0, - 'exchange': 'kraken' + 'leverage': 3.0 } @@ -2185,12 +2196,11 @@ def market_exit_short_order(): 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), 'price': 0.00004099, - 'amount': 91.99181073, - 'filled': 91.99181073, + 'amount': 276.113419906095, + 'filled': 276.113419906095, 'remaining': 0.0, 'status': 'closed', - # 'leverage': 3.0, - 'exchange': 'kraken' + 'leverage': 3.0 } @@ -2207,8 +2217,9 @@ def limit_leveraged_buy_order_open(): 'price': 0.00001099, 'amount': 272.97543219, 'filled': 0.0, - 'cost': 0.0029999999997681, + 'cost': 0.0009999999999226999, 'remaining': 272.97543219, + 'leverage': 3.0, 'status': 'open', 'exchange': 'binance' } @@ -2236,6 +2247,7 @@ def limit_leveraged_sell_order_open(): 'amount': 272.97543219, 'filled': 0.0, 'remaining': 272.97543219, + 'leverage': 3.0, 'status': 'open', 'exchange': 'binance' } diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index bc728dd44..f6b38f59a 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta, timezone from freqtrade.persistence.models import Order, Trade -MOCK_TRADE_COUNT = 6 # TODO-mg: Increase for short and leverage +MOCK_TRADE_COUNT = 6 def mock_order_1(): @@ -433,8 +433,7 @@ def leverage_trade(fee): interest_rate: 0.05% per day open_rate: 0.123 base close_rate: 0.128 base - amount: 123.0 crypto - amount_with_leverage: 615.0 + amount: 615 crypto stake_amount: 15.129 base borrowed: 60.516 base leverage: 5 diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index e324626c3..4fd6e716a 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -109,9 +109,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'exchange': 'binance', 'leverage': None, - 'borrowed': 0.0, - 'borrowed_currency': None, - 'collateral_currency': None, 'interest_rate': 0.0, 'liquidation_price': None, 'is_short': False, @@ -183,9 +180,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'exchange': 'binance', 'leverage': None, - 'borrowed': 0.0, - 'borrowed_currency': None, - 'collateral_currency': None, 'interest_rate': 0.0, 'liquidation_price': None, 'is_short': False, diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 484a8739a..74176ab49 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -105,27 +105,6 @@ def test_is_opening_closing_trade(fee): assert trade.is_closing_trade('sell') == False -@pytest.mark.usefixtures("init_persistence") -def test_amount(limit_buy_order, limit_sell_order, fee, caplog): - trade = Trade( - id=2, - pair='ETH/BTC', - stake_amount=0.001, - open_rate=0.01, - amount=5, - is_open=True, - open_date=arrow.utcnow().datetime, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - is_short=False - ) - assert trade.amount == 5 - trade.leverage = 3 - assert trade.amount == 15 - assert trade._amount == 5 - - @pytest.mark.usefixtures("init_persistence") def test_update_with_binance(limit_buy_order, limit_sell_order, fee, caplog): """ @@ -728,6 +707,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): FOREIGN KEY(ft_trade_id) REFERENCES trades (id) ) """)) + connection.execute(text(""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, remaining, cost, order_date, @@ -978,9 +958,6 @@ def test_to_json(default_conf, fee): 'exchange': 'binance', 'leverage': None, - 'borrowed': None, - 'borrowed_currency': None, - 'collateral_currency': None, 'interest_rate': None, 'liquidation_price': None, 'is_short': None, @@ -1051,9 +1028,6 @@ def test_to_json(default_conf, fee): 'exchange': 'binance', 'leverage': None, - 'borrowed': None, - 'borrowed_currency': None, - 'collateral_currency': None, 'interest_rate': None, 'liquidation_price': None, 'is_short': None, @@ -1189,42 +1163,6 @@ def test_fee_updated(fee): assert not trade.fee_updated('asfd') -@pytest.mark.usefixtures("init_persistence") -def test_update_leverage(fee, ten_minutes_ago): - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=5, - open_rate=0.00001099, - open_date=ten_minutes_ago, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - is_short=True, - interest_rate=0.0005 - ) - trade.leverage = 3.0 - assert trade.borrowed == 15.0 - assert trade.amount == 15.0 - - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=5, - open_rate=0.00001099, - open_date=ten_minutes_ago, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - is_short=False, - interest_rate=0.0005 - ) - - trade.leverage = 5.0 - assert trade.borrowed == 20.0 - assert trade.amount == 25.0 - - @pytest.mark.usefixtures("init_persistence") @pytest.mark.parametrize('use_db', [True, False]) def test_total_open_trades_stakes(fee, use_db): diff --git a/tests/test_persistence_long.py b/tests/test_persistence_long.py index cd0267cd1..98b6735e0 100644 --- a/tests/test_persistence_long.py +++ b/tests/test_persistence_long.py @@ -14,7 +14,358 @@ from tests.conftest import create_mock_trades_with_leverage, log_has, log_has_re @pytest.mark.usefixtures("init_persistence") -def test_update_with_binance(limit_leveraged_buy_order, limit_leveraged_sell_order, fee, ten_minutes_ago, caplog): +def test_interest_kraken(market_leveraged_buy_order, fee): + """ + Market trade on Kraken at 3x and 5x leverage + Short trade + interest_rate: 0.05%, 0.25% per 4 hrs + open_rate: 0.00004099 base + close_rate: 0.00004173 base + stake_amount: 0.0037707443218227 + borrowed: 0.0075414886436454 + amount: + 275.97543219 crypto + 459.95905365 crypto + borrowed: + 0.0075414886436454 base + 0.0150829772872908 base + time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) + 5 hours = 5/4 + + interest: borrowed * interest_rate * time-periods + = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 base + = 0.0075414886436454 * 0.00025 * 5/4 = 2.3567152011391876e-06 base + = 0.0150829772872908 * 0.0005 * 5/4 = 9.42686080455675e-06 base + = 0.0150829772872908 * 0.00025 * 1 = 3.7707443218227e-06 base + """ + + trade = Trade( + pair='ETH/BTC', + stake_amount=0.0037707443218227, + amount=275.97543219, + open_rate=0.00001099, + open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='kraken', + leverage=3.0, + interest_rate=0.0005 + ) + + # The trades that last 10 minutes do not need to be rounded because they round up to 4 hours on kraken so we can predict the correct value + assert float(trade.calculate_interest()) == 3.7707443218227e-06 + trade.open_date = datetime.utcnow() - timedelta(hours=5, minutes=0) + # The trades that last for 5 hours have to be rounded because the length of time that the test takes will vary every time it runs, so we can't predict the exact value + assert float(round(trade.calculate_interest(interest_rate=0.00025), 11) + ) == round(2.3567152011391876e-06, 11) + + trade = Trade( + pair='ETH/BTC', + stake_amount=0.0037707443218227, + amount=459.95905365, + open_rate=0.00001099, + open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='kraken', + leverage=5.0, + interest_rate=0.0005 + ) + + assert float(round(trade.calculate_interest(), 11) + ) == round(9.42686080455675e-06, 11) + trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10) + assert float(trade.calculate_interest(interest_rate=0.00025)) == 3.7707443218227e-06 + + +@pytest.mark.usefixtures("init_persistence") +def test_interest_binance(market_leveraged_buy_order, fee): + """ + Market trade on Kraken at 3x and 5x leverage + Short trade + interest_rate: 0.05%, 0.25% per 4 hrs + open_rate: 0.00001099 base + close_rate: 0.00001173 base + stake_amount: 0.0009999999999226999 + borrowed: 0.0019999999998453998 + amount: + 90.99181073 * leverage(3) = 272.97543219 crypto + 90.99181073 * leverage(5) = 454.95905365 crypto + borrowed: + 0.0019999999998453998 base + 0.0039999999996907995 base + time-periods: 10 minutes(rounds up to 1/24 time-period of 24hrs) + 5 hours = 5/24 + + interest: borrowed * interest_rate * time-periods + = 0.0019999999998453998 * 0.00050 * 1/24 = 4.166666666344583e-08 base + = 0.0019999999998453998 * 0.00025 * 5/24 = 1.0416666665861459e-07 base + = 0.0039999999996907995 * 0.00050 * 5/24 = 4.1666666663445834e-07 base + = 0.0039999999996907995 * 0.00025 * 1/24 = 4.166666666344583e-08 base + """ + + trade = Trade( + pair='ETH/BTC', + stake_amount=0.0009999999999226999, + amount=272.97543219, + open_rate=0.00001099, + open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + + leverage=3.0, + interest_rate=0.0005 + ) + # The trades that last 10 minutes do not always need to be rounded because they round up to 4 hours on kraken so we can predict the correct value + assert round(float(trade.calculate_interest()), 22) == round(4.166666666344583e-08, 22) + trade.open_date = datetime.utcnow() - timedelta(hours=5, minutes=0) + # The trades that last for 5 hours have to be rounded because the length of time that the test takes will vary every time it runs, so we can't predict the exact value + assert float(round(trade.calculate_interest(interest_rate=0.00025), 14) + ) == round(1.0416666665861459e-07, 14) + + trade = Trade( + pair='ETH/BTC', + stake_amount=0.0009999999999226999, + amount=459.95905365, + open_rate=0.00001099, + open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + leverage=5.0, + interest_rate=0.0005 + ) + + assert float(round(trade.calculate_interest(), 14)) == round(4.1666666663445834e-07, 14) + trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10) + assert float(round(trade.calculate_interest(interest_rate=0.00025), 22) + ) == round(4.166666666344583e-08, 22) + + +@pytest.mark.usefixtures("init_persistence") +def test_update_open_order(limit_leveraged_buy_order): + trade = Trade( + pair='ETH/BTC', + stake_amount=1.00, + open_rate=0.01, + amount=5, + fee_open=0.1, + fee_close=0.1, + interest_rate=0.0005, + leverage=3.0, + exchange='binance', + ) + assert trade.open_order_id is None + assert trade.close_profit is None + assert trade.close_date is None + limit_leveraged_buy_order['status'] = 'open' + trade.update(limit_leveraged_buy_order) + assert trade.open_order_id is None + assert trade.close_profit is None + assert trade.close_date is None + + +@pytest.mark.usefixtures("init_persistence") +def test_calc_open_trade_value(market_leveraged_buy_order, fee): + """ + 10 minute leveraged market trade on Kraken at 3x leverage + Short trade + fee: 0.25% base + interest_rate: 0.05% per 4 hrs + open_rate: 0.00004099 base + close_rate: 0.00004173 base + amount: 91.99181073 * leverage(3) = 275.97543219 crypto + stake_amount: 0.0037707443218227 + borrowed: 0.0075414886436454 base + time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) + interest: borrowed * interest_rate * time-periods + = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto + open_value: (amount * open_rate) + (amount * open_rate * fee) + = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) + = 0.01134051354788177 + """ + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + open_rate=0.00004099, + open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), + fee_open=fee.return_value, + fee_close=fee.return_value, + interest_rate=0.0005, + exchange='kraken', + leverage=3 + ) + trade.open_order_id = 'open_trade' + trade.update(market_leveraged_buy_order) # Buy @ 0.00001099 + # Get the open rate price with the standard fee rate + assert trade._calc_open_trade_value() == 0.01134051354788177 + trade.fee_open = 0.003 + # Get the open rate price with a custom fee rate + assert trade._calc_open_trade_value() == 0.011346169664364504 + + +@pytest.mark.usefixtures("init_persistence") +def test_calc_open_close_trade_price(limit_leveraged_buy_order, limit_leveraged_sell_order, fee): + """ + 5 hour leveraged trade on Binance + + fee: 0.25% base + interest_rate: 0.05% per day + open_rate: 0.00001099 base + close_rate: 0.00001173 base + amount: 272.97543219 crypto + stake_amount: 0.0009999999999226999 base + borrowed: 0.0019999999998453998 base + time-periods: 5 hours(rounds up to 5/24 time-period of 1 day) + interest: borrowed * interest_rate * time-periods + = 0.0019999999998453998 * 0.0005 * 5/24 = 2.0833333331722917e-07 base + open_value: (amount * open_rate) + (amount * open_rate * fee) + = (272.97543219 * 0.00001099) + (272.97543219 * 0.00001099 * 0.0025) + = 0.0030074999997675204 + close_value: ((amount_closed * close_rate) - (amount_closed * close_rate * fee)) - interest + = (272.97543219 * 0.00001173) - (272.97543219 * 0.00001173 * 0.0025) - 2.0833333331722917e-07 + = 0.003193788481706411 + total_profit = close_value - open_value + = 0.003193788481706411 - 0.0030074999997675204 + = 0.00018628848193889044 + total_profit_percentage = total_profit / stake_amount + = 0.00018628848193889054 / 0.0009999999999226999 + = 0.18628848195329067 + """ + trade = Trade( + pair='ETH/BTC', + stake_amount=0.0009999999999226999, + open_rate=0.01, + amount=5, + open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + interest_rate=0.0005 + ) + trade.open_order_id = 'something' + trade.update(limit_leveraged_buy_order) + assert trade._calc_open_trade_value() == 0.00300749999976752 + trade.update(limit_leveraged_sell_order) + + # Will be slightly different due to slight changes in compilation time, and the fact that interest depends on time + assert round(trade.calc_close_trade_value(), 11) == round(0.003193788481706411, 11) + # Profit in BTC + assert round(trade.calc_profit(), 8) == round(0.00018628848193889054, 8) + # Profit in percent + assert round(trade.calc_profit_ratio(), 8) == round(0.18628848195329067, 8) + + +@pytest.mark.usefixtures("init_persistence") +def test_trade_close(fee): + """ + 5 hour leveraged market trade on Kraken at 3x leverage + fee: 0.25% base + interest_rate: 0.05% per 4 hrs + open_rate: 0.1 base + close_rate: 0.2 base + amount: 5 * leverage(3) = 15 crypto + stake_amount: 0.5 + borrowed: 1 base + time-periods: 5/4 periods of 4hrs + interest: borrowed * interest_rate * time-periods + = 1 * 0.0005 * 5/4 = 0.000625 crypto + open_value: (amount * open_rate) + (amount * open_rate * fee) + = (15 * 0.1) + (15 * 0.1 * 0.0025) + = 1.50375 + close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - interest + = (15 * 0.2) - (15 * 0.2 * 0.0025) - 0.000625 + = 2.9918750000000003 + total_profit = close_value - open_value + = 2.9918750000000003 - 1.50375 + = 1.4881250000000001 + total_profit_percentage = total_profit / stake_amount + = 1.4881250000000001 / 0.5 + = 2.9762500000000003 + """ + trade = Trade( + pair='ETH/BTC', + stake_amount=0.5, + open_rate=0.1, + amount=15, + is_open=True, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), + exchange='kraken', + leverage=3.0, + interest_rate=0.0005 + ) + assert trade.close_profit is None + assert trade.close_date is None + assert trade.is_open is True + trade.close(0.2) + assert trade.is_open is False + assert trade.close_profit == round(2.9762500000000003, 8) + assert trade.close_date is not None + + # TODO-mg: Remove these comments probably + # new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, + # assert trade.close_date != new_date + # # Close should NOT update close_date if the trade has been closed already + # assert trade.is_open is False + # trade.close_date = new_date + # trade.close(0.02) + # assert trade.close_date == new_date + + +@pytest.mark.usefixtures("init_persistence") +def test_calc_close_trade_price(market_leveraged_buy_order, market_leveraged_sell_order, fee): + """ + 10 minute leveraged market trade on Kraken at 3x leverage + Short trade + fee: 0.25% base + interest_rate: 0.05% per 4 hrs + open_rate: 0.00004099 base + close_rate: 0.00004173 base + amount: 91.99181073 * leverage(3) = 275.97543219 crypto + stake_amount: 0.0037707443218227 + borrowed: 0.0075414886436454 base + time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) + interest: borrowed * interest_rate * time-periods + = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto + open_value: (amount * open_rate) + (amount * open_rate * fee) + = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) + = 0.01134051354788177 + close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest + = (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.0025) - 3.7707443218227e-06 = 0.003393252246819716 + = (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.003) - 3.7707443218227e-06 = 0.003391549478403104 + = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.005) - 3.7707443218227e-06 = 0.011455101767040435 + + """ + trade = Trade( + pair='ETH/BTC', + stake_amount=0.0037707443218227, + amount=5, + open_rate=0.00004099, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), + interest_rate=0.0005, + + leverage=3.0, + exchange='kraken', + ) + trade.open_order_id = 'close_trade' + trade.update(market_leveraged_buy_order) # Buy @ 0.00001099 + # Get the close rate price with a custom close rate and a regular fee rate + assert isclose(trade.calc_close_trade_value(rate=0.00001234), 0.003393252246819716) + # Get the close rate price with a custom close rate and a custom fee rate + assert isclose(trade.calc_close_trade_value(rate=0.00001234, fee=0.003), 0.003391549478403104) + # Test when we apply a Sell order, and ask price with a custom fee rate + trade.update(market_leveraged_sell_order) + assert isclose(trade.calc_close_trade_value(fee=0.005), 0.011455101767040435) + + +@pytest.mark.usefixtures("init_persistence") +def test_update_limit_order(limit_leveraged_buy_order, limit_leveraged_sell_order, fee, caplog): """ 10 minute leveraged limit trade on binance at 3x leverage @@ -50,18 +401,17 @@ def test_update_with_binance(limit_leveraged_buy_order, limit_leveraged_sell_ord open_rate=0.01, amount=5, is_open=True, - open_date=ten_minutes_ago, + open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), fee_open=fee.return_value, fee_close=fee.return_value, - # borrowed=90.99181073, + leverage=3.0, interest_rate=0.0005, exchange='binance' ) # assert trade.open_order_id is None assert trade.close_profit is None assert trade.close_date is None - assert trade.borrowed is None - assert trade.is_short is None + # trade.open_order_id = 'something' trade.update(limit_leveraged_buy_order) # assert trade.open_order_id is None @@ -69,7 +419,6 @@ def test_update_with_binance(limit_leveraged_buy_order, limit_leveraged_sell_ord assert trade.close_profit is None assert trade.close_date is None assert trade.borrowed == 0.0019999999998453998 - assert trade.is_short is True assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, " r"pair=ETH/BTC, amount=272.97543219, open_rate=0.00001099, open_since=.*\).", caplog) @@ -78,7 +427,7 @@ def test_update_with_binance(limit_leveraged_buy_order, limit_leveraged_sell_ord trade.update(limit_leveraged_sell_order) # assert trade.open_order_id is None assert trade.close_rate == 0.00001173 - assert trade.close_profit == 0.18645514861995735 + assert trade.close_profit == round(0.18645514861995735, 8) assert trade.close_date is not None assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, " r"pair=ETH/BTC, amount=272.97543219, open_rate=0.00001099, open_since=.*\).", @@ -86,7 +435,7 @@ def test_update_with_binance(limit_leveraged_buy_order, limit_leveraged_sell_ord @pytest.mark.usefixtures("init_persistence") -def test_update_market_order(limit_leveraged_buy_order, limit_leveraged_sell_order, fee, ten_minutes_ago, caplog): +def test_update_market_order(market_leveraged_buy_order, market_leveraged_sell_order, fee, caplog): """ 10 minute leveraged market trade on Kraken at 3x leverage Short trade @@ -94,7 +443,7 @@ def test_update_market_order(limit_leveraged_buy_order, limit_leveraged_sell_ord interest_rate: 0.05% per 4 hrs open_rate: 0.00004099 base close_rate: 0.00004173 base - amount: 91.99181073 * leverage(3) = 275.97543219 crypto + amount: = 275.97543219 crypto stake_amount: 0.0037707443218227 borrowed: 0.0075414886436454 base time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) @@ -118,19 +467,18 @@ def test_update_market_order(limit_leveraged_buy_order, limit_leveraged_sell_ord pair='ETH/BTC', stake_amount=0.0037707443218227, amount=5, - open_rate=0.01, + open_rate=0.00004099, is_open=True, leverage=3, fee_open=fee.return_value, fee_close=fee.return_value, - open_date=ten_minutes_ago, + open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), interest_rate=0.0005, exchange='kraken' ) trade.open_order_id = 'something' - trade.update(limit_leveraged_buy_order) + trade.update(market_leveraged_buy_order) assert trade.leverage == 3.0 - assert trade.is_short == True assert trade.open_order_id is None assert trade.open_rate == 0.00004099 assert trade.close_profit is None @@ -144,10 +492,10 @@ def test_update_market_order(limit_leveraged_buy_order, limit_leveraged_sell_ord caplog.clear() trade.is_open = True trade.open_order_id = 'something' - trade.update(limit_leveraged_sell_order) + trade.update(market_leveraged_sell_order) assert trade.open_order_id is None assert trade.close_rate == 0.00004173 - assert trade.close_profit == 0.03802415223225211 + assert trade.close_profit == round(0.03802415223225211, 8) assert trade.close_date is not None # TODO: The amount should maybe be the opening amount + the interest # TODO: Uncomment the next assert and make it work. @@ -157,116 +505,6 @@ def test_update_market_order(limit_leveraged_buy_order, limit_leveraged_sell_ord caplog) -@pytest.mark.usefixtures("init_persistence") -def test_calc_open_close_trade_price(limit_leveraged_buy_order, limit_leveraged_sell_order, five_hours_ago, fee): - """ - 5 hour leveraged trade on Binance - - fee: 0.25% base - interest_rate: 0.05% per day - open_rate: 0.00001099 base - close_rate: 0.00001173 base - amount: 272.97543219 crypto - stake_amount: 0.0009999999999226999 base - borrowed: 0.0019999999998453998 base - time-periods: 5 hours(rounds up to 5/24 time-period of 1 day) - interest: borrowed * interest_rate * time-periods - = 0.0019999999998453998 * 0.0005 * 5/24 = 2.0833333331722917e-07 base - open_value: (amount * open_rate) + (amount * open_rate * fee) - = (272.97543219 * 0.00001099) + (272.97543219 * 0.00001099 * 0.0025) - = 0.0030074999997675204 - close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - = (272.97543219 * 0.00001173) - (272.97543219 * 0.00001173 * 0.0025) - = 0.003193996815039728 - total_profit = close_value - open_value - interest - = 0.003193996815039728 - 0.0030074999997675204 - 2.0833333331722917e-07 - = 0.00018628848193889054 - total_profit_percentage = total_profit / stake_amount - = 0.00018628848193889054 / 0.0009999999999226999 - = 0.18628848195329067 - """ - trade = Trade( - pair='ETH/BTC', - stake_amount=0.0009999999999226999, - open_rate=0.01, - amount=5, - open_date=five_hours_ago, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - interest_rate=0.0005 - ) - trade.open_order_id = 'something' - trade.update(limit_leveraged_buy_order) - assert trade._calc_open_trade_value() == 0.0030074999997675204 - trade.update(limit_leveraged_sell_order) - - # Will be slightly different due to slight changes in compilation time, and the fact that interest depends on time - assert round(trade.calc_close_trade_value(), 11) == round(0.003193996815039728, 11) - # Profit in BTC - assert round(trade.calc_profit(), 8) == round(0.18628848195329067, 8) - # Profit in percent - # assert round(trade.calc_profit_ratio(), 11) == round(0.05822425142973869, 11) - - -@pytest.mark.usefixtures("init_persistence") -def test_trade_close(fee, five_hours_ago): - """ - 5 hour leveraged market trade on Kraken at 3x leverage - fee: 0.25% base - interest_rate: 0.05% per 4 hrs - open_rate: 0.1 base - close_rate: 0.2 base - amount: 5 * leverage(3) = 15 crypto - stake_amount: 0.5 - borrowed: 1 base - time-periods: 5/4 periods of 4hrs - interest: borrowed * interest_rate * time-periods - = 1 * 0.0005 * 5/4 = 0.000625 crypto - open_value: (amount * open_rate) + (amount * open_rate * fee) - = (15 * 0.1) + (15 * 0.1 * 0.0025) - = 1.50375 - close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - = (15 * 0.2) - (15 * 0.2 * 0.0025) - = 2.9925 - total_profit = close_value - open_value - interest - = 2.9925 - 1.50375 - 0.000625 - = 1.4881250000000001 - total_profit_percentage = total_profit / stake_amount - = 1.4881250000000001 / 0.5 - = 2.9762500000000003 - """ - trade = Trade( - pair='ETH/BTC', - stake_amount=0.1, - open_rate=0.01, - amount=5, - is_open=True, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_date=five_hours_ago, - exchange='kraken', - leverage=3.0, - interest_rate=0.0005 - ) - assert trade.close_profit is None - assert trade.close_date is None - assert trade.is_open is True - trade.close(0.02) - assert trade.is_open is False - assert trade.close_profit == round(2.9762500000000003, 8) - assert trade.close_date is not None - - # TODO-mg: Remove these comments probably - # new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, - # assert trade.close_date != new_date - # # Close should NOT update close_date if the trade has been closed already - # assert trade.is_open is False - # trade.close_date = new_date - # trade.close(0.02) - # assert trade.close_date == new_date - - @pytest.mark.usefixtures("init_persistence") def test_calc_close_trade_price_exception(limit_leveraged_buy_order, fee): trade = Trade( @@ -278,7 +516,7 @@ def test_calc_close_trade_price_exception(limit_leveraged_buy_order, fee): fee_close=fee.return_value, exchange='binance', interest_rate=0.0005, - borrowed=0.002 + leverage=3.0 ) trade.open_order_id = 'something' trade.update(limit_leveraged_buy_order) @@ -286,118 +524,7 @@ def test_calc_close_trade_price_exception(limit_leveraged_buy_order, fee): @pytest.mark.usefixtures("init_persistence") -def test_update_open_order(limit_leveraged_buy_order): - trade = Trade( - pair='ETH/BTC', - stake_amount=1.00, - open_rate=0.01, - amount=5, - fee_open=0.1, - fee_close=0.1, - interest_rate=0.0005, - borrowed=2.00, - exchange='binance', - ) - assert trade.open_order_id is None - assert trade.close_profit is None - assert trade.close_date is None - limit_leveraged_buy_order['status'] = 'open' - trade.update(limit_leveraged_buy_order) - assert trade.open_order_id is None - assert trade.close_profit is None - assert trade.close_date is None - - -@pytest.mark.usefixtures("init_persistence") -def test_calc_open_trade_value(market_leveraged_buy_order, ten_minutes_ago, fee): - """ - 10 minute leveraged market trade on Kraken at 3x leverage - Short trade - fee: 0.25% base - interest_rate: 0.05% per 4 hrs - open_rate: 0.00004099 base - close_rate: 0.00004173 base - amount: 91.99181073 * leverage(3) = 275.97543219 crypto - stake_amount: 0.0037707443218227 - borrowed: 0.0075414886436454 base - time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) - interest: borrowed * interest_rate * time-periods - = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto - open_value: (amount * open_rate) + (amount * open_rate * fee) - = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) - = 0.01134051354788177 - """ - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=5, - open_rate=0.00004099, - open_date=ten_minutes_ago, - fee_open=fee.return_value, - fee_close=fee.return_value, - interest_rate=0.0005, - exchange='kraken', - leverage=3 - ) - trade.open_order_id = 'open_trade' - trade.update(market_leveraged_buy_order) # Buy @ 0.00001099 - # Get the open rate price with the standard fee rate - assert trade._calc_open_trade_value() == 0.01134051354788177 - trade.fee_open = 0.003 - # Get the open rate price with a custom fee rate - assert trade._calc_open_trade_value() == 0.011346169664364504 - - -@pytest.mark.usefixtures("init_persistence") -def test_calc_close_trade_price(market_leveraged_buy_order, market_leveraged_sell_order, ten_minutes_ago, fee): - """ - 10 minute leveraged market trade on Kraken at 3x leverage - Short trade - fee: 0.25% base - interest_rate: 0.05% per 4 hrs - open_rate: 0.00004099 base - close_rate: 0.00004173 base - amount: 91.99181073 * leverage(3) = 275.97543219 crypto - stake_amount: 0.0037707443218227 - borrowed: 0.0075414886436454 base - time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) - interest: borrowed * interest_rate * time-periods - = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto - open_value: (amount * open_rate) + (amount * open_rate * fee) - = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) - = 0.01134051354788177 - close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - = (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.0025) = 0.0033970229911415386 - = (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.003) = 0.0033953202227249265 - = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.005) = 0.011458872511362258 - - """ - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=5, - open_rate=0.00001099, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_date=ten_minutes_ago, - interest_rate=0.0005, - is_short=True, - leverage=3.0, - exchange='kraken', - ) - trade.open_order_id = 'close_trade' - trade.update(market_leveraged_buy_order) # Buy @ 0.00001099 - # Get the close rate price with a custom close rate and a regular fee rate - assert isclose(trade.calc_close_trade_value(rate=0.00001234), 0.0033970229911415386) - # Get the close rate price with a custom close rate and a custom fee rate - assert isclose(trade.calc_close_trade_value(rate=0.00001234, fee=0.003), 0.0033953202227249265) - # Test when we apply a Sell order, and ask price with a custom fee rate - trade.update(market_leveraged_sell_order) - assert isclose(trade.calc_close_trade_value(fee=0.005), 0.011458872511362258) - - -@pytest.mark.usefixtures("init_persistence") -def test_calc_profit(market_leveraged_buy_order, market_leveraged_sell_order, ten_minutes_ago, five_hours_ago, fee): +def test_calc_profit(market_leveraged_buy_order, market_leveraged_sell_order, fee): """ # TODO: Update this one Leveraged trade on Kraken at 3x leverage @@ -412,35 +539,35 @@ def test_calc_profit(market_leveraged_buy_order, market_leveraged_sell_order, te 5 hours = 5/4 interest: borrowed * interest_rate * time-periods - = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto + = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto = 0.0075414886436454 * 0.00025 * 5/4 = 2.3567152011391876e-06 crypto - = 0.0075414886436454 * 0.0005 * 5/4 = 4.713430402278375e-06 crypto - = 0.0075414886436454 * 0.00025 * 1 = 1.88537216091135e-06 crypto + = 0.0075414886436454 * 0.0005 * 5/4 = 4.713430402278375e-06 crypto + = 0.0075414886436454 * 0.00025 * 1 = 1.88537216091135e-06 crypto open_value: (amount * open_rate) + (amount * open_rate * fee) = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) = 0.01134051354788177 - close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - (275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.0025) = 0.014793842426575873 - (275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.0025) = 0.0012029976070736241 - (275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.003) = 0.014786426966712927 - (275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.003) = 0.0012023946007542888 + close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest + (275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.0025) - 3.7707443218227e-06 = 0.01479007168225405 + (275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.0025) - 2.3567152011391876e-06 = 0.001200640891872485 + (275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.003) - 4.713430402278375e-06 = 0.014781713536310649 + (275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.003) - 1.88537216091135e-06 = 0.0012005092285933775 total_profit = close_value - open_value - = 0.014793842426575873 - 0.01134051354788177 = 0.003453328878694104 - = 0.0012029976070736241 - 0.01134051354788177 = -0.010137515940808145 - = 0.014786426966712927 - 0.01134051354788177 = 0.0034459134188311574 - = 0.0012023946007542888 - 0.01134051354788177 = -0.01013811894712748 + = 0.01479007168225405 - 0.01134051354788177 = 0.003449558134372281 + = 0.001200640891872485 - 0.01134051354788177 = -0.010139872656009285 + = 0.014781713536310649 - 0.01134051354788177 = 0.0034411999884288794 + = 0.0012005092285933775 - 0.01134051354788177 = -0.010140004319288392 total_profit_percentage = total_profit / stake_amount - 0.003453328878694104/0.0037707443218227 = 0.9158215418394733 - -0.010137515940808145/0.0037707443218227 = -2.6884654793852154 - 0.0034459134188311574/0.0037707443218227 = 0.9138549646255183 - -0.01013811894712748/0.0037707443218227 = -2.6886253964381557 + 0.003449558134372281/0.0037707443218227 = 0.9148215418394732 + -0.010139872656009285/0.0037707443218227 = -2.6890904793852157 + 0.0034411999884288794/0.0037707443218227 = 0.9126049646255184 + -0.010140004319288392/0.0037707443218227 = -2.6891253964381554 """ trade = Trade( pair='ETH/BTC', - stake_amount=0.0038388182617629, + stake_amount=0.0037707443218227, amount=5, open_rate=0.00004099, - open_date=ten_minutes_ago, + open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), fee_open=fee.return_value, fee_close=fee.return_value, exchange='kraken', @@ -452,31 +579,31 @@ def test_calc_profit(market_leveraged_buy_order, market_leveraged_sell_order, te # Custom closing rate and regular fee rate # Higher than open rate - assert trade.calc_profit(rate=0.00004374, interest_rate=0.0005) == round( - 0.003453328878694104, 8) + assert trade.calc_profit(rate=0.00005374, interest_rate=0.0005) == round( + 0.003449558134372281, 8) assert trade.calc_profit_ratio( - rate=0.00004374, interest_rate=0.0005) == round(0.9158215418394733, 8) + rate=0.00005374, interest_rate=0.0005) == round(0.9148215418394732, 8) # Lower than open rate - trade.open_date = five_hours_ago + trade.open_date = datetime.utcnow() - timedelta(hours=5, minutes=0) assert trade.calc_profit( - rate=0.00000437, interest_rate=0.00025) == round(-0.010137515940808145, 8) + rate=0.00000437, interest_rate=0.00025) == round(-0.010139872656009285, 8) assert trade.calc_profit_ratio( - rate=0.00000437, interest_rate=0.00025) == round(-2.6884654793852154, 8) + rate=0.00000437, interest_rate=0.00025) == round(-2.6890904793852157, 8) # Custom closing rate and custom fee rate # Higher than open rate - assert trade.calc_profit(rate=0.00004374, fee=0.003, - interest_rate=0.0005) == round(0.0034459134188311574, 8) - assert trade.calc_profit_ratio(rate=0.00004374, fee=0.003, - interest_rate=0.0005) == round(0.9138549646255183, 8) + assert trade.calc_profit(rate=0.00005374, fee=0.003, + interest_rate=0.0005) == round(0.0034411999884288794, 8) + assert trade.calc_profit_ratio(rate=0.00005374, fee=0.003, + interest_rate=0.0005) == round(0.9126049646255184, 8) # Lower than open rate - trade.open_date = ten_minutes_ago + trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10) assert trade.calc_profit(rate=0.00000437, fee=0.003, - interest_rate=0.00025) == round(-0.01013811894712748, 8) + interest_rate=0.00025) == round(-0.010140004319288392, 8) assert trade.calc_profit_ratio(rate=0.00000437, fee=0.003, - interest_rate=0.00025) == round(-2.6886253964381557, 8) + interest_rate=0.00025) == round(-2.6891253964381554, 8) # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 trade.update(market_leveraged_sell_order) @@ -486,131 +613,3 @@ def test_calc_profit(market_leveraged_buy_order, market_leveraged_sell_order, te # Test with a custom fee rate on the close trade # assert trade.calc_profit(fee=0.003) == 0.00006163 # assert trade.calc_profit_ratio(fee=0.003) == 0.06147824 - - -@pytest.mark.usefixtures("init_persistence") -def test_interest_kraken(market_leveraged_buy_order, ten_minutes_ago, five_hours_ago, fee): - """ - Market trade on Kraken at 3x and 8x leverage - Short trade - interest_rate: 0.05%, 0.25% per 4 hrs - open_rate: 0.00004099 base - close_rate: 0.00004173 base - stake_amount: 0.0037707443218227 - borrowed: 0.0075414886436454 - amount: - 91.99181073 * leverage(3) = 275.97543219 crypto - 91.99181073 * leverage(5) = 459.95905365 crypto - borrowed: - 0.0075414886436454 base - 0.0150829772872908 base - time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) - 5 hours = 5/4 - - interest: borrowed * interest_rate * time-periods - = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 base - = 0.0075414886436454 * 0.00025 * 5/4 = 2.3567152011391876e-06 base - = 0.0150829772872908 * 0.0005 * 5/4 = 9.42686080455675e-06 base - = 0.0150829772872908 * 0.00025 * 1 = 3.7707443218227e-06 base - """ - - trade = Trade( - pair='ETH/BTC', - stake_amount=0.0037707443218227, - amount=91.99181073, - open_rate=0.00001099, - open_date=ten_minutes_ago, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='kraken', - leverage=3.0, - interest_rate=0.0005 - ) - - assert float(round(trade.calculate_interest(), 8)) == 3.7707443218227e-06 - trade.open_date = five_hours_ago - assert float(round(trade.calculate_interest(interest_rate=0.00025), 8) - ) == 2.3567152011391876e-06 # TODO: Fails with 0.08624233 - - trade = Trade( - pair='ETH/BTC', - stake_amount=0.0037707443218227, - amount=91.99181073, - open_rate=0.00001099, - open_date=five_hours_ago, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='kraken', - is_short=True, - leverage=5.0, - interest_rate=0.0005 - ) - - assert float(round(trade.calculate_interest(), 8) - ) == 9.42686080455675e-06 # TODO: Fails with 0.28747445 - trade.open_date = ten_minutes_ago - assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)) == 3.7707443218227e-06 - - -@pytest.mark.usefixtures("init_persistence") -def test_interest_binance(market_leveraged_buy_order, ten_minutes_ago, five_hours_ago, fee): - """ - Market trade on Kraken at 3x and 8x leverage - Short trade - interest_rate: 0.05%, 0.25% per 4 hrs - open_rate: 0.00004099 base - close_rate: 0.00004173 base - stake_amount: 0.0037707443218227 - borrowed: 0.0075414886436454 - amount: - 91.99181073 * leverage(3) = 275.97543219 crypto - 91.99181073 * leverage(5) = 459.95905365 crypto - borrowed: - 0.0075414886436454 base - 0.0150829772872908 base - time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) - 5 hours = 5/24 - - interest: borrowed * interest_rate * time-periods - = 0.0075414886436454 * 0.0005 * 1/24 = 1.571143467426125e-07 base - = 0.0075414886436454 * 0.00025 * 5/24 = 3.9278586685653125e-07 base - = 0.0150829772872908 * 0.0005 * 5/24 = 1.571143467426125e-06 base - = 0.0150829772872908 * 0.00025 * 1/24 = 1.571143467426125e-07 base - """ - - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=275.97543219, - open_rate=0.00001099, - open_date=ten_minutes_ago, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - is_short=True, - borrowed=275.97543219, - interest_rate=0.0005 - ) - - assert float(round(trade.calculate_interest(), 8)) == 1.571143467426125e-07 - trade.open_date = five_hours_ago - assert float(round(trade.calculate_interest(interest_rate=0.00025), 8) - ) == 3.9278586685653125e-07 - - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=459.95905365, - open_rate=0.00001099, - open_date=five_hours_ago, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - is_short=True, - borrowed=459.95905365, - interest_rate=0.0005 - ) - - assert float(round(trade.calculate_interest(), 8)) == 1.571143467426125e-06 - trade.open_date = ten_minutes_ago - assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)) == 1.571143467426125e-07 diff --git a/tests/test_persistence_short.py b/tests/test_persistence_short.py index 759b25a1a..c9abff4b0 100644 --- a/tests/test_persistence_short.py +++ b/tests/test_persistence_short.py @@ -14,7 +14,357 @@ from tests.conftest import create_mock_trades_with_leverage, log_has, log_has_re @pytest.mark.usefixtures("init_persistence") -def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, ten_minutes_ago, caplog): +def test_interest_kraken(market_short_order, fee): + """ + Market trade on Kraken at 3x and 8x leverage + Short trade + interest_rate: 0.05%, 0.25% per 4 hrs + open_rate: 0.00004173 base + close_rate: 0.00004099 base + amount: + 275.97543219 crypto + 459.95905365 crypto + borrowed: + 275.97543219 crypto + 459.95905365 crypto + time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) + 5 hours = 5/4 + + interest: borrowed * interest_rate * time-periods + = 275.97543219 * 0.0005 * 1 = 0.137987716095 crypto + = 275.97543219 * 0.00025 * 5/4 = 0.086242322559375 crypto + = 459.95905365 * 0.0005 * 5/4 = 0.28747440853125 crypto + = 459.95905365 * 0.00025 * 1 = 0.1149897634125 crypto + """ + + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=275.97543219, + open_rate=0.00001099, + open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='kraken', + is_short=True, + leverage=3.0, + interest_rate=0.0005 + ) + + assert float(round(trade.calculate_interest(), 8)) == round(0.137987716095, 8) + trade.open_date = datetime.utcnow() - timedelta(hours=5, minutes=0) + assert float(round(trade.calculate_interest(interest_rate=0.00025), 8) + ) == round(0.086242322559375, 8) + + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=459.95905365, + open_rate=0.00001099, + open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='kraken', + is_short=True, + leverage=5.0, + interest_rate=0.0005 + ) + + assert float(round(trade.calculate_interest(), 8)) == round(0.28747440853125, 8) + trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10) + assert float(round(trade.calculate_interest(interest_rate=0.00025), 8) + ) == round(0.1149897634125, 8) + + +@ pytest.mark.usefixtures("init_persistence") +def test_interest_binance(market_short_order, fee): + """ + Market trade on Binance at 3x and 5x leverage + Short trade + interest_rate: 0.05%, 0.25% per 1 day + open_rate: 0.00004173 base + close_rate: 0.00004099 base + amount: + 91.99181073 * leverage(3) = 275.97543219 crypto + 91.99181073 * leverage(5) = 459.95905365 crypto + borrowed: + 275.97543219 crypto + 459.95905365 crypto + time-periods: 10 minutes(rounds up to 1/24 time-period of 1 day) + 5 hours = 5/24 + + interest: borrowed * interest_rate * time-periods + = 275.97543219 * 0.0005 * 1/24 = 0.005749488170625 crypto + = 275.97543219 * 0.00025 * 5/24 = 0.0143737204265625 crypto + = 459.95905365 * 0.0005 * 5/24 = 0.047912401421875 crypto + = 459.95905365 * 0.00025 * 1/24 = 0.0047912401421875 crypto + """ + + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=275.97543219, + open_rate=0.00001099, + open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + is_short=True, + leverage=3.0, + interest_rate=0.0005 + ) + + assert float(round(trade.calculate_interest(), 8)) == 0.00574949 + trade.open_date = datetime.utcnow() - timedelta(hours=5, minutes=0) + assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)) == 0.01437372 + + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=459.95905365, + open_rate=0.00001099, + open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + is_short=True, + leverage=5.0, + interest_rate=0.0005 + ) + + assert float(round(trade.calculate_interest(), 8)) == 0.04791240 + trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10) + assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)) == 0.00479124 + + +@ pytest.mark.usefixtures("init_persistence") +def test_calc_open_trade_value(market_short_order, fee): + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + open_rate=0.00004173, + open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), + fee_open=fee.return_value, + fee_close=fee.return_value, + interest_rate=0.0005, + is_short=True, + leverage=3.0, + exchange='kraken', + ) + trade.open_order_id = 'open_trade' + trade.update(market_short_order) # Buy @ 0.00001099 + # Get the open rate price with the standard fee rate + assert trade._calc_open_trade_value() == 0.011487663648325479 + trade.fee_open = 0.003 + # Get the open rate price with a custom fee rate + assert trade._calc_open_trade_value() == 0.011481905420932834 + + +@ pytest.mark.usefixtures("init_persistence") +def test_update_open_order(limit_short_order): + trade = Trade( + pair='ETH/BTC', + stake_amount=1.00, + open_rate=0.01, + amount=5, + leverage=3.0, + fee_open=0.1, + fee_close=0.1, + interest_rate=0.0005, + is_short=True, + exchange='binance', + ) + assert trade.open_order_id is None + assert trade.close_profit is None + assert trade.close_date is None + limit_short_order['status'] = 'open' + trade.update(limit_short_order) + assert trade.open_order_id is None + assert trade.close_profit is None + assert trade.close_date is None + + +@ pytest.mark.usefixtures("init_persistence") +def test_calc_close_trade_price_exception(limit_short_order, fee): + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + open_rate=0.1, + amount=15.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + interest_rate=0.0005, + leverage=3.0, + is_short=True + ) + trade.open_order_id = 'something' + trade.update(limit_short_order) + assert trade.calc_close_trade_value() == 0.0 + + +@ pytest.mark.usefixtures("init_persistence") +def test_calc_close_trade_price(market_short_order, market_exit_short_order, fee): + """ + 10 minute short market trade on Kraken at 3x leverage + Short trade + fee: 0.25% base + interest_rate: 0.05% per 4 hrs + open_rate: 0.00004173 base + close_rate: 0.00001234 base + amount: = 275.97543219 crypto + borrowed: 275.97543219 crypto + time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) + interest: borrowed * interest_rate * time-periods + = 275.97543219 * 0.0005 * 1 = 0.137987716095 crypto + amount_closed: amount + interest = 275.97543219 + 0.137987716095 = 276.113419906095 + close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) + = (276.113419906095 * 0.00001234) + (276.113419906095 * 0.00001234 * 0.0025) + = 0.01134618380465571 + """ + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + open_rate=0.00001099, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), + interest_rate=0.0005, + is_short=True, + leverage=3.0, + exchange='kraken', + ) + trade.open_order_id = 'close_trade' + trade.update(market_short_order) # Buy @ 0.00001099 + # Get the close rate price with a custom close rate and a regular fee rate + assert isclose(trade.calc_close_trade_value(rate=0.00001234), 0.003415757700645315) + # Get the close rate price with a custom close rate and a custom fee rate + assert isclose(trade.calc_close_trade_value(rate=0.00001234, fee=0.003), 0.0034174613204461354) + # Test when we apply a Sell order, and ask price with a custom fee rate + trade.update(market_exit_short_order) + assert isclose(trade.calc_close_trade_value(fee=0.005), 0.011374478527360586) + + +@ pytest.mark.usefixtures("init_persistence") +def test_calc_open_close_trade_price(limit_short_order, limit_exit_short_order, fee): + """ + 5 hour short trade on Binance + Short trade + fee: 0.25% base + interest_rate: 0.05% per day + open_rate: 0.00001173 base + close_rate: 0.00001099 base + amount: 90.99181073 crypto + borrowed: 90.99181073 crypto + stake_amount: 0.0010673339398629 + time-periods: 5 hours = 5/24 + interest: borrowed * interest_rate * time-periods + = 90.99181073 * 0.0005 * 5/24 = 0.009478313617708333 crypto + open_value: (amount * open_rate) - (amount * open_rate * fee) + = (90.99181073 * 0.00001173) - (90.99181073 * 0.00001173 * 0.0025) + = 0.0010646656050132426 + amount_closed: amount + interest = 90.99181073 + 0.009478313617708333 = 91.0012890436177 + close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) + = (91.0012890436177 * 0.00001099) + (91.0012890436177 * 0.00001099 * 0.0025) + = 0.001002604427005832 + total_profit = open_value - close_value + = 0.0010646656050132426 - 0.001002604427005832 + = 0.00006206117800741065 + total_profit_percentage = (close_value - open_value) / stake_amount + = (0.0010646656050132426 - 0.0010025208853391716)/0.0010673339398629 + = 0.05822425142973869 + """ + trade = Trade( + pair='ETH/BTC', + stake_amount=0.0010673339398629, + open_rate=0.01, + amount=5, + open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + interest_rate=0.0005 + ) + trade.open_order_id = 'something' + trade.update(limit_short_order) + assert trade._calc_open_trade_value() == 0.0010646656050132426 + trade.update(limit_exit_short_order) + + # Will be slightly different due to slight changes in compilation time, and the fact that interest depends on time + assert round(trade.calc_close_trade_value(), 11) == round(0.001002604427005832, 11) + # Profit in BTC + assert round(trade.calc_profit(), 8) == round(0.00006206117800741065, 8) + # Profit in percent + # assert round(trade.calc_profit_ratio(), 11) == round(0.05822425142973869, 11) + + +@ pytest.mark.usefixtures("init_persistence") +def test_trade_close(fee): + """ + Five hour short trade on Kraken at 3x leverage + Short trade + Exchange: Kraken + fee: 0.25% base + interest_rate: 0.05% per 4 hours + open_rate: 0.02 base + close_rate: 0.01 base + leverage: 3.0 + amount: 15 crypto + borrowed: 15 crypto + time-periods: 5 hours = 5/4 + + interest: borrowed * interest_rate * time-periods + = 15 * 0.0005 * 5/4 = 0.009375 crypto + open_value: (amount * open_rate) - (amount * open_rate * fee) + = (15 * 0.02) - (15 * 0.02 * 0.0025) + = 0.29925 + amount_closed: amount + interest = 15 + 0.009375 = 15.009375 + close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) + = (15.009375 * 0.01) + (15.009375 * 0.01 * 0.0025) + = 0.150468984375 + total_profit = open_value - close_value + = 0.29925 - 0.150468984375 + = 0.148781015625 + total_profit_percentage = total_profit / stake_amount + = 0.148781015625 / 0.1 + = 1.4878101562500001 + """ + trade = Trade( + pair='ETH/BTC', + stake_amount=0.1, + open_rate=0.02, + amount=15, + is_open=True, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), + exchange='kraken', + is_short=True, + leverage=3.0, + interest_rate=0.0005 + ) + assert trade.close_profit is None + assert trade.close_date is None + assert trade.is_open is True + trade.close(0.01) + assert trade.is_open is False + assert trade.close_profit == round(1.4878101562500001, 8) + assert trade.close_date is not None + + # TODO-mg: Remove these comments probably + # new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, + # assert trade.close_date != new_date + # # Close should NOT update close_date if the trade has been closed already + # assert trade.is_open is False + # trade.close_date = new_date + # trade.close(0.02) + # assert trade.close_date == new_date + + +@ pytest.mark.usefixtures("init_persistence") +def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, caplog): """ 10 minute short limit trade on binance @@ -40,7 +390,7 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, ten = 0.0010646656050132426 - 0.0010025208853391716 = 0.00006214471967407108 total_profit_percentage = (close_value - open_value) / stake_amount - = (0.0010646656050132426 - 0.0010025208853391716) / 0.0010673339398629 + = 0.00006214471967407108 / 0.0010673339398629 = 0.05822425142973869 """ @@ -51,7 +401,7 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, ten open_rate=0.01, amount=5, is_open=True, - open_date=ten_minutes_ago, + open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), fee_open=fee.return_value, fee_close=fee.return_value, # borrowed=90.99181073, @@ -61,7 +411,7 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, ten # assert trade.open_order_id is None assert trade.close_profit is None assert trade.close_date is None - assert trade.borrowed is None + assert trade.borrowed == 0.0 assert trade.is_short is None # trade.open_order_id = 'something' trade.update(limit_short_order) @@ -86,12 +436,11 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, ten caplog) -@pytest.mark.usefixtures("init_persistence") +@ pytest.mark.usefixtures("init_persistence") def test_update_market_order( market_short_order, market_exit_short_order, fee, - ten_minutes_ago, caplog ): """ @@ -101,7 +450,7 @@ def test_update_market_order( interest_rate: 0.05% per 4 hrs open_rate: 0.00004173 base close_rate: 0.00004099 base - amount: 91.99181073 * leverage(3) = 275.97543219 crypto + amount: = 275.97543219 crypto stake_amount: 0.0038388182617629 borrowed: 275.97543219 crypto time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) @@ -130,7 +479,8 @@ def test_update_market_order( is_open=True, fee_open=fee.return_value, fee_close=fee.return_value, - open_date=ten_minutes_ago, + open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), + leverage=3.0, interest_rate=0.0005, exchange='kraken' ) @@ -164,233 +514,8 @@ def test_update_market_order( # caplog) -@pytest.mark.usefixtures("init_persistence") -def test_calc_open_close_trade_price(limit_short_order, limit_exit_short_order, five_hours_ago, fee): - """ - 5 hour short trade on Binance - Short trade - fee: 0.25% base - interest_rate: 0.05% per day - open_rate: 0.00001173 base - close_rate: 0.00001099 base - amount: 90.99181073 crypto - borrowed: 90.99181073 crypto - stake_amount: 0.0010673339398629 - time-periods: 5 hours = 5/24 - interest: borrowed * interest_rate * time-periods - = 90.99181073 * 0.0005 * 5/24 = 0.009478313617708333 crypto - open_value: (amount * open_rate) - (amount * open_rate * fee) - = (90.99181073 * 0.00001173) - (90.99181073 * 0.00001173 * 0.0025) - = 0.0010646656050132426 - amount_closed: amount + interest = 90.99181073 + 0.009478313617708333 = 91.0012890436177 - close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - = (91.0012890436177 * 0.00001099) + (91.0012890436177 * 0.00001099 * 0.0025) - = 0.001002604427005832 - total_profit = open_value - close_value - = 0.0010646656050132426 - 0.001002604427005832 - = 0.00006206117800741065 - total_profit_percentage = (close_value - open_value) / stake_amount - = (0.0010646656050132426 - 0.0010025208853391716)/0.0010673339398629 - = 0.05822425142973869 - """ - trade = Trade( - pair='ETH/BTC', - stake_amount=0.0010673339398629, - open_rate=0.01, - amount=5, - open_date=five_hours_ago, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - interest_rate=0.0005 - ) - trade.open_order_id = 'something' - trade.update(limit_short_order) - assert trade._calc_open_trade_value() == 0.0010646656050132426 - trade.update(limit_exit_short_order) - - # Will be slightly different due to slight changes in compilation time, and the fact that interest depends on time - assert round(trade.calc_close_trade_value(), 11) == round(0.001002604427005832, 11) - # Profit in BTC - assert round(trade.calc_profit(), 8) == round(0.00006206117800741065, 8) - # Profit in percent - # assert round(trade.calc_profit_ratio(), 11) == round(0.05822425142973869, 11) - - -@pytest.mark.usefixtures("init_persistence") -def test_trade_close(fee, five_hours_ago): - """ - Five hour short trade on Kraken at 3x leverage - Short trade - Exchange: Kraken - fee: 0.25% base - interest_rate: 0.05% per 4 hours - open_rate: 0.02 base - close_rate: 0.01 base - leverage: 3.0 - amount: 5 * 3 = 15 crypto - borrowed: 15 crypto - time-periods: 5 hours = 5/4 - - interest: borrowed * interest_rate * time-periods - = 15 * 0.0005 * 5/4 = 0.009375 crypto - open_value: (amount * open_rate) - (amount * open_rate * fee) - = (15 * 0.02) - (15 * 0.02 * 0.0025) - = 0.29925 - amount_closed: amount + interest = 15 + 0.009375 = 15.009375 - close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - = (15.009375 * 0.01) + (15.009375 * 0.01 * 0.0025) - = 0.150468984375 - total_profit = open_value - close_value - = 0.29925 - 0.150468984375 - = 0.148781015625 - total_profit_percentage = total_profit / stake_amount - = 0.148781015625 / 0.1 - = 1.4878101562500001 - """ - trade = Trade( - pair='ETH/BTC', - stake_amount=0.1, - open_rate=0.02, - amount=5, - is_open=True, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_date=five_hours_ago, - exchange='kraken', - is_short=True, - leverage=3.0, - interest_rate=0.0005 - ) - assert trade.close_profit is None - assert trade.close_date is None - assert trade.is_open is True - trade.close(0.01) - assert trade.is_open is False - assert trade.close_profit == round(1.4878101562500001, 8) - assert trade.close_date is not None - - # TODO-mg: Remove these comments probably - # new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, - # assert trade.close_date != new_date - # # Close should NOT update close_date if the trade has been closed already - # assert trade.is_open is False - # trade.close_date = new_date - # trade.close(0.02) - # assert trade.close_date == new_date - - -@pytest.mark.usefixtures("init_persistence") -def test_calc_close_trade_price_exception(limit_short_order, fee): - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - open_rate=0.1, - amount=5, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - interest_rate=0.0005, - is_short=True, - borrowed=15 - ) - trade.open_order_id = 'something' - trade.update(limit_short_order) - assert trade.calc_close_trade_value() == 0.0 - - -@pytest.mark.usefixtures("init_persistence") -def test_update_open_order(limit_short_order): - trade = Trade( - pair='ETH/BTC', - stake_amount=1.00, - open_rate=0.01, - amount=5, - fee_open=0.1, - fee_close=0.1, - interest_rate=0.0005, - is_short=True, - exchange='binance', - ) - assert trade.open_order_id is None - assert trade.close_profit is None - assert trade.close_date is None - limit_short_order['status'] = 'open' - trade.update(limit_short_order) - assert trade.open_order_id is None - assert trade.close_profit is None - assert trade.close_date is None - - -@pytest.mark.usefixtures("init_persistence") -def test_calc_open_trade_value(market_short_order, ten_minutes_ago, fee): - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=5, - open_rate=0.00004173, - open_date=ten_minutes_ago, - fee_open=fee.return_value, - fee_close=fee.return_value, - interest_rate=0.0005, - is_short=True, - leverage=3.0, - exchange='kraken', - ) - trade.open_order_id = 'open_trade' - trade.update(market_short_order) # Buy @ 0.00001099 - # Get the open rate price with the standard fee rate - assert trade._calc_open_trade_value() == 0.011487663648325479 - trade.fee_open = 0.003 - # Get the open rate price with a custom fee rate - assert trade._calc_open_trade_value() == 0.011481905420932834 - - -@pytest.mark.usefixtures("init_persistence") -def test_calc_close_trade_price(market_short_order, market_exit_short_order, ten_minutes_ago, fee): - """ - 10 minute short market trade on Kraken at 3x leverage - Short trade - fee: 0.25% base - interest_rate: 0.05% per 4 hrs - open_rate: 0.00004173 base - close_rate: 0.00001234 base - amount: 91.99181073 * leverage(3) = 275.97543219 crypto - borrowed: 275.97543219 crypto - time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) - interest: borrowed * interest_rate * time-periods - = 275.97543219 * 0.0005 * 1 = 0.137987716095 crypto - amount_closed: amount + interest = 275.97543219 + 0.137987716095 = 276.113419906095 - close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - = (276.113419906095 * 0.00001234) + (276.113419906095 * 0.00001234 * 0.0025) - = 0.01134618380465571 - """ - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=5, - open_rate=0.00001099, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_date=ten_minutes_ago, - interest_rate=0.0005, - is_short=True, - leverage=3.0, - exchange='kraken', - ) - trade.open_order_id = 'close_trade' - trade.update(market_short_order) # Buy @ 0.00001099 - # Get the close rate price with a custom close rate and a regular fee rate - assert isclose(trade.calc_close_trade_value(rate=0.00001234), 0.003415757700645315) - # Get the close rate price with a custom close rate and a custom fee rate - assert isclose(trade.calc_close_trade_value(rate=0.00001234, fee=0.003), 0.0034174613204461354) - # Test when we apply a Sell order, and ask price with a custom fee rate - trade.update(market_exit_short_order) - assert isclose(trade.calc_close_trade_value(fee=0.005), 0.011374478527360586) - - -@pytest.mark.usefixtures("init_persistence") -def test_calc_profit(market_short_order, market_exit_short_order, ten_minutes_ago, five_hours_ago, fee): +@ pytest.mark.usefixtures("init_persistence") +def test_calc_profit(market_short_order, market_exit_short_order, fee): """ Market trade on Kraken at 3x leverage Short trade @@ -399,7 +524,7 @@ def test_calc_profit(market_short_order, market_exit_short_order, ten_minutes_ag open_rate: 0.00004173 base close_rate: 0.00004099 base stake_amount: 0.0038388182617629 - amount: 91.99181073 * leverage(3) = 275.97543219 crypto + amount: = 275.97543219 crypto borrowed: 275.97543219 crypto time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) 5 hours = 5/4 @@ -438,7 +563,7 @@ def test_calc_profit(market_short_order, market_exit_short_order, ten_minutes_ag stake_amount=0.0038388182617629, amount=5, open_rate=0.00001099, - open_date=ten_minutes_ago, + open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), fee_open=fee.return_value, fee_close=fee.return_value, exchange='kraken', @@ -456,7 +581,7 @@ def test_calc_profit(market_short_order, market_exit_short_order, ten_minutes_ag rate=0.00004374, interest_rate=0.0005) == round(-0.16143779115744006, 8) # Lower than open rate - trade.open_date = five_hours_ago + trade.open_date = datetime.utcnow() - timedelta(hours=5, minutes=0) assert trade.calc_profit(rate=0.00000437, interest_rate=0.00025) == round(0.01027826, 8) assert trade.calc_profit_ratio( rate=0.00000437, interest_rate=0.00025) == round(2.677453699564163, 8) @@ -469,7 +594,7 @@ def test_calc_profit(market_short_order, market_exit_short_order, ten_minutes_ag interest_rate=0.0005) == round(-0.16340506919482353, 8) # Lower than open rate - trade.open_date = ten_minutes_ago + trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10) assert trade.calc_profit(rate=0.00000437, fee=0.003, interest_rate=0.00025) == round(0.01027773, 8) assert trade.calc_profit_ratio(rate=0.00000437, fee=0.003, @@ -485,129 +610,6 @@ def test_calc_profit(market_short_order, market_exit_short_order, ten_minutes_ag # assert trade.calc_profit_ratio(fee=0.003) == 0.06147824 -@pytest.mark.usefixtures("init_persistence") -def test_interest_kraken(market_short_order, ten_minutes_ago, five_hours_ago, fee): - """ - Market trade on Kraken at 3x and 8x leverage - Short trade - interest_rate: 0.05%, 0.25% per 4 hrs - open_rate: 0.00004173 base - close_rate: 0.00004099 base - amount: - 91.99181073 * leverage(3) = 275.97543219 crypto - 91.99181073 * leverage(5) = 459.95905365 crypto - borrowed: - 275.97543219 crypto - 459.95905365 crypto - time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) - 5 hours = 5/4 - - interest: borrowed * interest_rate * time-periods - = 275.97543219 * 0.0005 * 1 = 0.137987716095 crypto - = 275.97543219 * 0.00025 * 5/4 = 0.086242322559375 crypto - = 459.95905365 * 0.0005 * 5/4 = 0.28747440853125 crypto - = 459.95905365 * 0.00025 * 1 = 0.1149897634125 crypto - """ - - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=91.99181073, - open_rate=0.00001099, - open_date=ten_minutes_ago, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='kraken', - is_short=True, - leverage=3.0, - interest_rate=0.0005 - ) - - assert float(round(trade.calculate_interest(), 8)) == 0.13798772 - trade.open_date = five_hours_ago - assert float(round(trade.calculate_interest(interest_rate=0.00025), 8) - ) == 0.08624232 # TODO: Fails with 0.08624233 - - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=91.99181073, - open_rate=0.00001099, - open_date=five_hours_ago, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='kraken', - is_short=True, - leverage=5.0, - interest_rate=0.0005 - ) - - assert float(round(trade.calculate_interest(), 8)) == 0.28747441 # TODO: Fails with 0.28747445 - trade.open_date = ten_minutes_ago - assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)) == 0.11498976 - - -@pytest.mark.usefixtures("init_persistence") -def test_interest_binance(market_short_order, ten_minutes_ago, five_hours_ago, fee): - """ - Market trade on Binance at 3x and 5x leverage - Short trade - interest_rate: 0.05%, 0.25% per 1 day - open_rate: 0.00004173 base - close_rate: 0.00004099 base - amount: - 91.99181073 * leverage(3) = 275.97543219 crypto - 91.99181073 * leverage(5) = 459.95905365 crypto - borrowed: - 275.97543219 crypto - 459.95905365 crypto - time-periods: 10 minutes(rounds up to 1/24 time-period of 1 day) - 5 hours = 5/24 - - interest: borrowed * interest_rate * time-periods - = 275.97543219 * 0.0005 * 1/24 = 0.005749488170625 crypto - = 275.97543219 * 0.00025 * 5/24 = 0.0143737204265625 crypto - = 459.95905365 * 0.0005 * 5/24 = 0.047912401421875 crypto - = 459.95905365 * 0.00025 * 1/24 = 0.0047912401421875 crypto - """ - - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=275.97543219, - open_rate=0.00001099, - open_date=ten_minutes_ago, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - is_short=True, - borrowed=275.97543219, - interest_rate=0.0005 - ) - - assert float(round(trade.calculate_interest(), 8)) == 0.00574949 - trade.open_date = five_hours_ago - assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)) == 0.01437372 - - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=459.95905365, - open_rate=0.00001099, - open_date=five_hours_ago, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - is_short=True, - borrowed=459.95905365, - interest_rate=0.0005 - ) - - assert float(round(trade.calculate_interest(), 8)) == 0.04791240 - trade.open_date = ten_minutes_ago - assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)) == 0.00479124 - - def test_adjust_stop_loss(fee): trade = Trade( pair='ETH/BTC', @@ -653,11 +655,13 @@ def test_adjust_stop_loss(fee): assert trade.initial_stop_loss == 1.05 assert trade.initial_stop_loss_pct == 0.05 assert trade.stop_loss_pct == 0.1 + trade.liquidation_price == 1.03 + # TODO-mg: Do a test with a trade that has a liquidation price -@pytest.mark.usefixtures("init_persistence") -@pytest.mark.parametrize('use_db', [True, False]) +@ pytest.mark.usefixtures("init_persistence") +@ pytest.mark.parametrize('use_db', [True, False]) def test_get_open(fee, use_db): Trade.use_db = use_db Trade.reset_trades() @@ -679,7 +683,8 @@ def test_stoploss_reinitialization(default_conf, fee): exchange='binance', open_rate=1, max_rate=1, - is_short=True + is_short=True, + leverage=3.0, ) trade.adjust_stop_loss(trade.open_rate, -0.05, True) assert trade.stop_loss == 1.05 @@ -720,8 +725,8 @@ def test_stoploss_reinitialization(default_conf, fee): assert trade_adj.initial_stop_loss_pct == 0.04 -@pytest.mark.usefixtures("init_persistence") -@pytest.mark.parametrize('use_db', [True, False]) +@ pytest.mark.usefixtures("init_persistence") +@ pytest.mark.parametrize('use_db', [True, False]) def test_total_open_trades_stakes(fee, use_db): Trade.use_db = use_db Trade.reset_trades() @@ -733,7 +738,7 @@ def test_total_open_trades_stakes(fee, use_db): Trade.use_db = True -@pytest.mark.usefixtures("init_persistence") +@ pytest.mark.usefixtures("init_persistence") def test_get_best_pair(fee): res = Trade.get_best_pair() assert res is None From 0d5749c5088eb1e9aad9c22e0416ea415286ae97 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 4 Jul 2021 23:12:07 -0600 Subject: [PATCH 19/46] Set default leverage to 1.0 --- freqtrade/persistence/migrations.py | 4 ++-- freqtrade/persistence/models.py | 8 ++++---- tests/conftest_trades.py | 3 --- tests/rpc/test_rpc.py | 6 ++---- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 8e2f708d5..fbf8d7943 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -48,7 +48,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') - leverage = get_column_def(cols, 'leverage', 'null') + leverage = get_column_def(cols, 'leverage', '1.0') interest_rate = get_column_def(cols, 'interest_rate', '0.0') liquidation_price = get_column_def(cols, 'liquidation_price', 'null') is_short = get_column_def(cols, 'is_short', 'False') @@ -146,7 +146,7 @@ def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, col # let SQLAlchemy create the schema as required decl_base.metadata.create_all(engine) - leverage = get_column_def(cols, 'leverage', 'null') + leverage = get_column_def(cols, 'leverage', '1.0') is_short = get_column_def(cols, 'is_short', 'False') with engine.begin() as connection: connection.execute(text(f""" diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index ebfae72b9..a22ff6238 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -132,7 +132,7 @@ class Order(_DECL_BASE): order_filled_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True) - leverage = Column(Float, nullable=True, default=None) + leverage = Column(Float, nullable=True, default=1.0) is_short = Column(Boolean, nullable=True, default=False) def __repr__(self): @@ -267,7 +267,7 @@ class LocalTrade(): interest_rate: float = 0.0 liquidation_price: float = None is_short: bool = False - leverage: float = None + leverage: float = 1.0 @property def has_no_leverage(self) -> bool: @@ -583,7 +583,7 @@ class LocalTrade(): zero = Decimal(0.0) # If nothing was borrowed - if (self.leverage == 1.0 and not self.is_short) or not self.leverage: + if self.has_no_leverage: return zero open_date = self.open_date.replace(tzinfo=None) @@ -853,7 +853,7 @@ class Trade(_DECL_BASE, LocalTrade): timeframe = Column(Integer, nullable=True) # Margin trading properties - leverage = Column(Float, nullable=True) + leverage = Column(Float, nullable=True, default=1.0) interest_rate = Column(Float, nullable=False, default=0.0) liquidation_price = Column(Float, nullable=True) is_short = Column(Boolean, nullable=False, default=False) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index f6b38f59a..e46186039 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -305,9 +305,6 @@ def mock_trade_6(fee): return trade -#! TODO Currently the following short_trade test and leverage_trade test will fail - - def short_order(): return { 'id': '1236', diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 4fd6e716a..3650aa57b 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -107,8 +107,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', - - 'leverage': None, + 'leverage': 1.0, 'interest_rate': 0.0, 'liquidation_price': None, 'is_short': False, @@ -178,8 +177,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', - - 'leverage': None, + 'leverage': 1.0, 'interest_rate': 0.0, 'liquidation_price': None, 'is_short': False, From c5ce8c6dd8fda8f5fcd2bb8b2f8c49b563c99833 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 4 Jul 2021 23:32:55 -0600 Subject: [PATCH 20/46] fixed rpc_apiserver test fails, changed test_persistence_long to test_persistence_leverage --- tests/conftest.py | 2 ++ .../{test_persistence_long.py => test_persistence_leverage.py} | 0 2 files changed, 2 insertions(+) rename tests/{test_persistence_long.py => test_persistence_leverage.py} (100%) diff --git a/tests/conftest.py b/tests/conftest.py index 843769df0..f935b7fa2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -215,6 +215,8 @@ def create_mock_trades(fee, use_db: bool = True): add_trade(trade) trade = mock_trade_6(fee) add_trade(trade) + if use_db: + Trade.query.session.flush() def create_mock_trades_with_leverage(fee, use_db: bool = True): diff --git a/tests/test_persistence_long.py b/tests/test_persistence_leverage.py similarity index 100% rename from tests/test_persistence_long.py rename to tests/test_persistence_leverage.py From ffadc7426c8c0c1a1ebf98bb9daca34f67fd9f29 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 4 Jul 2021 23:50:59 -0600 Subject: [PATCH 21/46] Removed exchange file modifications --- freqtrade/exchange/binance.py | 10 ---------- freqtrade/exchange/kraken.py | 9 --------- 2 files changed, 19 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index a8d60d6c0..0c470cb24 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -3,7 +3,6 @@ import logging from typing import Dict import ccxt -from decimal import Decimal from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) @@ -90,12 +89,3 @@ class Binance(Exchange): f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e - - @staticmethod - def calculate_interest(borrowed: Decimal, hours: Decimal, interest_rate: Decimal) -> Decimal: - # Rate is per day but accrued hourly or something - # binance: https://www.binance.com/en-AU/support/faq/360030157812 - one = Decimal(1) - twenty_four = Decimal(24) - # TODO-mg: Is hours rounded? - return borrowed * interest_rate * max(hours, one)/twenty_four diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 2cd2ac118..1b069aa6c 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -3,7 +3,6 @@ import logging from typing import Any, Dict import ccxt -from decimal import Decimal from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) @@ -125,11 +124,3 @@ class Kraken(Exchange): f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e - - @staticmethod - def calculate_interest(borrowed: Decimal, hours: Decimal, interest_rate: Decimal) -> Decimal: - four = Decimal(4.0) - # https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading- - opening_fee = borrowed * interest_rate - roll_over_fee = borrowed * interest_rate * max(0, (hours-four)/four) - return opening_fee + roll_over_fee From b6c8b60e65fd51f7f9fb945b02a6e2e74dd57971 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 4 Jul 2021 23:51:58 -0600 Subject: [PATCH 22/46] Switched migrations.py check for stake_currency back to open_rate, because stake_currency is no longer a variable --- freqtrade/persistence/migrations.py | 5 ++-- tests/conftest_trades.py | 36 ++++++++++++++--------------- tests/test_persistence.py | 2 -- tests/test_persistence_short.py | 14 +++++------ 4 files changed, 27 insertions(+), 30 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index fbf8d7943..69ffc544e 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -171,9 +171,8 @@ def check_migrate(engine, decl_base, previous_tables) -> None: tabs = get_table_names_for_table(inspector, 'trades') table_back_name = get_backup_name(tabs, 'trades_bak') - # Last added column of trades table - # To determine if migrations need to run - if not has_column(cols, 'collateral_currency'): + # Check for latest column + if not has_column(cols, 'open_trade_value'): logger.info(f'Running database migration for trades - backup: {table_back_name}') migrate_trades_table(decl_base, inspector, engine, table_back_name, cols) # Reread columns - the above recreated the table! diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index e46186039..915cecd35 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -436,33 +436,33 @@ def leverage_trade(fee): leverage: 5 time-periods: 5 hrs( 5/4 time-period of 4 hours) interest: borrowed * interest_rate * time-periods - = 60.516 * 0.0005 * 1/24 = 0.0378225 base - open_value: (amount * open_rate) - (amount * open_rate * fee) - = (615.0 * 0.123) - (615.0 * 0.123 * 0.0025) - = 75.4558875 + = 60.516 * 0.0005 * 5/4 = 0.0378225 base + open_value: (amount * open_rate) + (amount * open_rate * fee) + = (615.0 * 0.123) + (615.0 * 0.123 * 0.0025) + = 75.83411249999999 - close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - = (615.0 * 0.128) + (615.0 * 0.128 * 0.0025) - = 78.9168 - total_profit = close_value - open_value - interest - = 78.9168 - 75.4558875 - 0.0378225 - = 3.423089999999992 + close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest + = (615.0 * 0.128) - (615.0 * 0.128 * 0.0025) - 0.0378225 + = 78.4853775 + total_profit = close_value - open_value + = 78.4853775 - 75.83411249999999 + = 2.6512650000000093 total_profit_percentage = total_profit / stake_amount - = 3.423089999999992 / 15.129 - = 0.22626016260162551 + = 2.6512650000000093 / 15.129 + = 0.17524390243902502 """ trade = Trade( pair='ETC/BTC', stake_amount=15.129, - amount=123.0, - leverage=5, - amount_requested=123.0, + amount=615.0, + leverage=5.0, + amount_requested=615.0, fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, close_rate=0.128, - close_profit=0.22626016260162551, - close_profit_abs=3.423089999999992, + close_profit=0.17524390243902502, + close_profit_abs=2.6512650000000093, exchange='kraken', is_open=False, open_order_id='dry_run_leverage_sell_12345', @@ -471,7 +471,7 @@ def leverage_trade(fee): sell_reason='sell_signal', # TODO-mg: Update to exit/close reason open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=300), close_date=datetime.now(tz=timezone.utc), - # borrowed= + interest_rate=0.0005 ) o = Order.parse_from_ccxt_object(leverage_order(), 'ETC/BTC', 'sell') trade.orders.append(o) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 74176ab49..68ebca3b1 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -956,7 +956,6 @@ def test_to_json(default_conf, fee): 'strategy': None, 'timeframe': None, 'exchange': 'binance', - 'leverage': None, 'interest_rate': None, 'liquidation_price': None, @@ -1026,7 +1025,6 @@ def test_to_json(default_conf, fee): 'strategy': None, 'timeframe': None, 'exchange': 'binance', - 'leverage': None, 'interest_rate': None, 'liquidation_price': None, diff --git a/tests/test_persistence_short.py b/tests/test_persistence_short.py index c9abff4b0..6c8d9e4f0 100644 --- a/tests/test_persistence_short.py +++ b/tests/test_persistence_short.py @@ -495,9 +495,9 @@ def test_update_market_order( assert trade.interest_rate == 0.0005 # TODO: Uncomment the next assert and make it work. # The logger also has the exact same but there's some spacing in there - # assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " - # r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004173, open_since=.*\).", - # caplog) + assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " + r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004173, open_since=.*\).", + caplog) caplog.clear() trade.is_open = True trade.open_order_id = 'something' @@ -509,9 +509,9 @@ def test_update_market_order( # TODO: The amount should maybe be the opening amount + the interest # TODO: Uncomment the next assert and make it work. # The logger also has the exact same but there's some spacing in there - # assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " - # r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004099, open_since=.*\).", - # caplog) + assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " + r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004099, open_since=.*\).", + caplog) @ pytest.mark.usefixtures("init_persistence") @@ -746,4 +746,4 @@ def test_get_best_pair(fee): res = Trade.get_best_pair() assert len(res) == 2 assert res[0] == 'ETC/BTC' - assert res[1] == 0.22626016260162551 + assert res[1] == 0.17524390243902502 From d48f1083b06303a915b945c2e496dc310a236c36 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 5 Jul 2021 00:56:32 -0600 Subject: [PATCH 23/46] updated leverage.md --- docs/leverage.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/docs/leverage.md b/docs/leverage.md index eee1d00bb..9a420e573 100644 --- a/docs/leverage.md +++ b/docs/leverage.md @@ -1,10 +1,3 @@ -An instance of a `Trade`/`LocalTrade` object is given either a value for `leverage` or a value for `borrowed`, but not both, on instantiation/update with a short/long. - -- If given a value for `leverage`, then the `amount` value of the `Trade`/`Local` object is multiplied by the `leverage` value to obtain the new value for `amount`. The borrowed value is also calculated from the `amount` and `leverage` value -- If given a value for `borrowed`, then the `leverage` value is left as None - For shorts, the currency which pays the interest fee for the `borrowed` currency is purchased at the same time of the closing trade (This means that the amount purchased in short closing trades is greater than the amount sold in short opening trades). -For longs, the currency which pays the interest fee for the `borrowed` will already be owned by the user and does not need to be purchased - -The interest fee is paid following the closing trade, or simultaneously depending on the exchange +For longs, the currency which pays the interest fee for the `borrowed` will already be owned by the user and does not need to be purchased. The interest is subtracted from the close_value of the trade. From 1b202ca22e2133c288f86541079fbbe9a733676b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 5 Jul 2021 21:48:56 -0600 Subject: [PATCH 24/46] Moved interest calculation to an enum --- freqtrade/enums/__init__.py | 1 + freqtrade/enums/interestmode.py | 30 +++++++++++++++++++++++ freqtrade/persistence/models.py | 30 ++++++----------------- tests/test_persistence_leverage.py | 38 ++++++++++++++++++++---------- tests/test_persistence_short.py | 38 +++++++++++++++++++++--------- 5 files changed, 90 insertions(+), 47 deletions(-) create mode 100644 freqtrade/enums/interestmode.py diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index ac5f804c9..179d2d5e9 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa: F401 from freqtrade.enums.backteststate import BacktestState +from freqtrade.enums.interestmode import InterestMode from freqtrade.enums.rpcmessagetype import RPCMessageType from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.selltype import SellType diff --git a/freqtrade/enums/interestmode.py b/freqtrade/enums/interestmode.py new file mode 100644 index 000000000..c95f4731f --- /dev/null +++ b/freqtrade/enums/interestmode.py @@ -0,0 +1,30 @@ +from enum import Enum, auto +from decimal import Decimal + +one = Decimal(1.0) +four = Decimal(4.0) +twenty_four = Decimal(24.0) + + +class FunctionProxy: + """Allow to mask a function as an Object.""" + + def __init__(self, function): + self.function = function + + def __call__(self, *args, **kwargs): + return self.function(*args, **kwargs) + + +class InterestMode(Enum): + """Equations to calculate interest""" + + # Interest_rate is per day, minimum time of 1 hour + HOURSPERDAY = FunctionProxy( + lambda borrowed, rate, hours: borrowed * rate * max(hours, one)/twenty_four + ) + + # Interest_rate is per 4 hours, minimum time of 4 hours + HOURSPER4 = FunctionProxy( + lambda borrowed, rate, hours: borrowed * rate * (1 + max(0, (hours-four)/four)) + ) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a22ff6238..54a5676d9 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -14,7 +14,7 @@ from sqlalchemy.pool import StaticPool from sqlalchemy.sql.schema import UniqueConstraint from freqtrade.constants import DATETIME_PRINT_FORMAT -from freqtrade.enums import SellType +from freqtrade.enums import InterestMode, SellType from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate @@ -265,9 +265,10 @@ class LocalTrade(): # Margin trading properties interest_rate: float = 0.0 - liquidation_price: float = None + liquidation_price: Optional[float] = None is_short: bool = False leverage: float = 1.0 + interest_mode: Optional[InterestMode] = None @property def has_no_leverage(self) -> bool: @@ -585,6 +586,8 @@ class LocalTrade(): # If nothing was borrowed if self.has_no_leverage: return zero + elif not self.interest_mode: + raise OperationalException(f"Leverage not available on {self.exchange} using freqtrade") open_date = self.open_date.replace(tzinfo=None) now = (self.close_date or datetime.utcnow()).replace(tzinfo=None) @@ -594,28 +597,8 @@ class LocalTrade(): rate = Decimal(interest_rate or self.interest_rate) borrowed = Decimal(self.borrowed) - one = Decimal(1.0) - twenty_four = Decimal(24.0) - four = Decimal(4.0) - if self.exchange == 'binance': - # Rate is per day but accrued hourly or something - # binance: https://www.binance.com/en-AU/support/faq/360030157812 - return borrowed * rate * max(hours, one)/twenty_four - 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-four)/four) - return opening_fee + roll_over_fee - elif self.exchange == 'binance_usdm_futures': - # ! TODO-mg: This is incorrect, I didn't look it up - return borrowed * (rate/twenty_four) * max(hours, one) - elif self.exchange == 'binance_coinm_futures': - # ! TODO-mg: This is incorrect, I didn't look it up - return borrowed * (rate/twenty_four) * max(hours, one) - else: - # TODO-mg: make sure this breaks and can't be squelched - raise OperationalException("Leverage not available on this exchange") + return self.interest_mode.value(borrowed, rate, hours) def calc_close_trade_value(self, rate: Optional[float] = None, fee: Optional[float] = None, @@ -857,6 +840,7 @@ class Trade(_DECL_BASE, LocalTrade): interest_rate = Column(Float, nullable=False, default=0.0) liquidation_price = Column(Float, nullable=True) is_short = Column(Boolean, nullable=False, default=False) + interest_mode = Column(String(100), nullable=True) # End of margin trading properties def __init__(self, **kwargs): diff --git a/tests/test_persistence_leverage.py b/tests/test_persistence_leverage.py index 98b6735e0..7850a134f 100644 --- a/tests/test_persistence_leverage.py +++ b/tests/test_persistence_leverage.py @@ -8,6 +8,7 @@ import pytest from math import isclose from sqlalchemy import create_engine, inspect, text from freqtrade import constants +from freqtrade.enums import InterestMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db from tests.conftest import create_mock_trades_with_leverage, log_has, log_has_re @@ -49,7 +50,8 @@ def test_interest_kraken(market_leveraged_buy_order, fee): fee_close=fee.return_value, exchange='kraken', leverage=3.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPER4 ) # The trades that last 10 minutes do not need to be rounded because they round up to 4 hours on kraken so we can predict the correct value @@ -69,7 +71,8 @@ def test_interest_kraken(market_leveraged_buy_order, fee): fee_close=fee.return_value, exchange='kraken', leverage=5.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPER4 ) assert float(round(trade.calculate_interest(), 11) @@ -113,9 +116,9 @@ def test_interest_binance(market_leveraged_buy_order, fee): fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', - leverage=3.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY ) # The trades that last 10 minutes do not always need to be rounded because they round up to 4 hours on kraken so we can predict the correct value assert round(float(trade.calculate_interest()), 22) == round(4.166666666344583e-08, 22) @@ -134,7 +137,8 @@ def test_interest_binance(market_leveraged_buy_order, fee): fee_close=fee.return_value, exchange='binance', leverage=5.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY ) assert float(round(trade.calculate_interest(), 14)) == round(4.1666666663445834e-07, 14) @@ -155,6 +159,7 @@ def test_update_open_order(limit_leveraged_buy_order): interest_rate=0.0005, leverage=3.0, exchange='binance', + interest_mode=InterestMode.HOURSPERDAY ) assert trade.open_order_id is None assert trade.close_profit is None @@ -195,7 +200,8 @@ def test_calc_open_trade_value(market_leveraged_buy_order, fee): fee_close=fee.return_value, interest_rate=0.0005, exchange='kraken', - leverage=3 + leverage=3, + interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'open_trade' trade.update(market_leveraged_buy_order) # Buy @ 0.00001099 @@ -243,7 +249,8 @@ def test_calc_open_close_trade_price(limit_leveraged_buy_order, limit_leveraged_ fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY ) trade.open_order_id = 'something' trade.update(limit_leveraged_buy_order) @@ -296,7 +303,8 @@ def test_trade_close(fee): open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), exchange='kraken', leverage=3.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPER4 ) assert trade.close_profit is None assert trade.close_date is None @@ -349,9 +357,9 @@ def test_calc_close_trade_price(market_leveraged_buy_order, market_leveraged_sel fee_close=fee.return_value, open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), interest_rate=0.0005, - leverage=3.0, exchange='kraken', + interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'close_trade' trade.update(market_leveraged_buy_order) # Buy @ 0.00001099 @@ -406,7 +414,8 @@ def test_update_limit_order(limit_leveraged_buy_order, limit_leveraged_sell_orde fee_close=fee.return_value, leverage=3.0, interest_rate=0.0005, - exchange='binance' + exchange='binance', + interest_mode=InterestMode.HOURSPERDAY ) # assert trade.open_order_id is None assert trade.close_profit is None @@ -474,7 +483,8 @@ def test_update_market_order(market_leveraged_buy_order, market_leveraged_sell_o fee_close=fee.return_value, open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), interest_rate=0.0005, - exchange='kraken' + exchange='kraken', + interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'something' trade.update(market_leveraged_buy_order) @@ -516,7 +526,8 @@ def test_calc_close_trade_price_exception(limit_leveraged_buy_order, fee): fee_close=fee.return_value, exchange='binance', interest_rate=0.0005, - leverage=3.0 + leverage=3.0, + interest_mode=InterestMode.HOURSPERDAY ) trade.open_order_id = 'something' trade.update(limit_leveraged_buy_order) @@ -572,7 +583,8 @@ def test_calc_profit(market_leveraged_buy_order, market_leveraged_sell_order, fe fee_close=fee.return_value, exchange='kraken', leverage=3.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'something' trade.update(market_leveraged_buy_order) # Buy @ 0.00001099 diff --git a/tests/test_persistence_short.py b/tests/test_persistence_short.py index 6c8d9e4f0..1f39f7439 100644 --- a/tests/test_persistence_short.py +++ b/tests/test_persistence_short.py @@ -8,6 +8,7 @@ import pytest from math import isclose from sqlalchemy import create_engine, inspect, text from freqtrade import constants +from freqtrade.enums import InterestMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db from tests.conftest import create_mock_trades_with_leverage, log_has, log_has_re @@ -48,7 +49,8 @@ def test_interest_kraken(market_short_order, fee): exchange='kraken', is_short=True, leverage=3.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPER4 ) assert float(round(trade.calculate_interest(), 8)) == round(0.137987716095, 8) @@ -67,7 +69,8 @@ def test_interest_kraken(market_short_order, fee): exchange='kraken', is_short=True, leverage=5.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPER4 ) assert float(round(trade.calculate_interest(), 8)) == round(0.28747440853125, 8) @@ -111,7 +114,8 @@ def test_interest_binance(market_short_order, fee): exchange='binance', is_short=True, leverage=3.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY ) assert float(round(trade.calculate_interest(), 8)) == 0.00574949 @@ -129,7 +133,8 @@ def test_interest_binance(market_short_order, fee): exchange='binance', is_short=True, leverage=5.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY ) assert float(round(trade.calculate_interest(), 8)) == 0.04791240 @@ -151,6 +156,7 @@ def test_calc_open_trade_value(market_short_order, fee): is_short=True, leverage=3.0, exchange='kraken', + interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'open_trade' trade.update(market_short_order) # Buy @ 0.00001099 @@ -174,6 +180,7 @@ def test_update_open_order(limit_short_order): interest_rate=0.0005, is_short=True, exchange='binance', + interest_mode=InterestMode.HOURSPERDAY ) assert trade.open_order_id is None assert trade.close_profit is None @@ -197,7 +204,8 @@ def test_calc_close_trade_price_exception(limit_short_order, fee): exchange='binance', interest_rate=0.0005, leverage=3.0, - is_short=True + is_short=True, + interest_mode=InterestMode.HOURSPERDAY ) trade.open_order_id = 'something' trade.update(limit_short_order) @@ -235,6 +243,7 @@ def test_calc_close_trade_price(market_short_order, market_exit_short_order, fee is_short=True, leverage=3.0, exchange='kraken', + interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'close_trade' trade.update(market_short_order) # Buy @ 0.00001099 @@ -285,7 +294,8 @@ def test_calc_open_close_trade_price(limit_short_order, limit_exit_short_order, fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY ) trade.open_order_id = 'something' trade.update(limit_short_order) @@ -343,7 +353,8 @@ def test_trade_close(fee): exchange='kraken', is_short=True, leverage=3.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPER4 ) assert trade.close_profit is None assert trade.close_date is None @@ -406,7 +417,8 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, cap fee_close=fee.return_value, # borrowed=90.99181073, interest_rate=0.0005, - exchange='binance' + exchange='binance', + interest_mode=InterestMode.HOURSPERDAY ) # assert trade.open_order_id is None assert trade.close_profit is None @@ -482,7 +494,8 @@ def test_update_market_order( open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), leverage=3.0, interest_rate=0.0005, - exchange='kraken' + exchange='kraken', + interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'something' trade.update(market_short_order) @@ -569,7 +582,8 @@ def test_calc_profit(market_short_order, market_exit_short_order, fee): exchange='kraken', is_short=True, leverage=3.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'something' trade.update(market_short_order) # Buy @ 0.00001099 @@ -620,7 +634,8 @@ def test_adjust_stop_loss(fee): exchange='binance', open_rate=1, max_rate=1, - is_short=True + is_short=True, + interest_mode=InterestMode.HOURSPERDAY ) trade.adjust_stop_loss(trade.open_rate, 0.05, True) assert trade.stop_loss == 1.05 @@ -685,6 +700,7 @@ def test_stoploss_reinitialization(default_conf, fee): max_rate=1, is_short=True, leverage=3.0, + interest_mode=InterestMode.HOURSPERDAY ) trade.adjust_stop_loss(trade.open_rate, -0.05, True) assert trade.stop_loss == 1.05 From 3328707a1df733a98c5343d02218da75aaab9316 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 5 Jul 2021 22:01:46 -0600 Subject: [PATCH 25/46] made leveraged test names unique test_adjust_stop_loss_short, test_update_market_order_shortpasses --- tests/{ => persistence}/test_persistence.py | 0 .../test_persistence_leverage.py | 22 ++++---- .../test_persistence_short.py | 50 +++++++++---------- 3 files changed, 36 insertions(+), 36 deletions(-) rename tests/{ => persistence}/test_persistence.py (100%) rename tests/{ => persistence}/test_persistence_leverage.py (96%) rename tests/{ => persistence}/test_persistence_short.py (95%) diff --git a/tests/test_persistence.py b/tests/persistence/test_persistence.py similarity index 100% rename from tests/test_persistence.py rename to tests/persistence/test_persistence.py diff --git a/tests/test_persistence_leverage.py b/tests/persistence/test_persistence_leverage.py similarity index 96% rename from tests/test_persistence_leverage.py rename to tests/persistence/test_persistence_leverage.py index 7850a134f..44da84f37 100644 --- a/tests/test_persistence_leverage.py +++ b/tests/persistence/test_persistence_leverage.py @@ -15,7 +15,7 @@ from tests.conftest import create_mock_trades_with_leverage, log_has, log_has_re @pytest.mark.usefixtures("init_persistence") -def test_interest_kraken(market_leveraged_buy_order, fee): +def test_interest_kraken_lev(market_leveraged_buy_order, fee): """ Market trade on Kraken at 3x and 5x leverage Short trade @@ -82,7 +82,7 @@ def test_interest_kraken(market_leveraged_buy_order, fee): @pytest.mark.usefixtures("init_persistence") -def test_interest_binance(market_leveraged_buy_order, fee): +def test_interest_binance_lev(market_leveraged_buy_order, fee): """ Market trade on Kraken at 3x and 5x leverage Short trade @@ -148,7 +148,7 @@ def test_interest_binance(market_leveraged_buy_order, fee): @pytest.mark.usefixtures("init_persistence") -def test_update_open_order(limit_leveraged_buy_order): +def test_update_open_order_lev(limit_leveraged_buy_order): trade = Trade( pair='ETH/BTC', stake_amount=1.00, @@ -172,7 +172,7 @@ def test_update_open_order(limit_leveraged_buy_order): @pytest.mark.usefixtures("init_persistence") -def test_calc_open_trade_value(market_leveraged_buy_order, fee): +def test_calc_open_trade_value_lev(market_leveraged_buy_order, fee): """ 10 minute leveraged market trade on Kraken at 3x leverage Short trade @@ -213,7 +213,7 @@ def test_calc_open_trade_value(market_leveraged_buy_order, fee): @pytest.mark.usefixtures("init_persistence") -def test_calc_open_close_trade_price(limit_leveraged_buy_order, limit_leveraged_sell_order, fee): +def test_calc_open_close_trade_price_lev(limit_leveraged_buy_order, limit_leveraged_sell_order, fee): """ 5 hour leveraged trade on Binance @@ -266,7 +266,7 @@ def test_calc_open_close_trade_price(limit_leveraged_buy_order, limit_leveraged_ @pytest.mark.usefixtures("init_persistence") -def test_trade_close(fee): +def test_trade_close_lev(fee): """ 5 hour leveraged market trade on Kraken at 3x leverage fee: 0.25% base @@ -325,7 +325,7 @@ def test_trade_close(fee): @pytest.mark.usefixtures("init_persistence") -def test_calc_close_trade_price(market_leveraged_buy_order, market_leveraged_sell_order, fee): +def test_calc_close_trade_price_lev(market_leveraged_buy_order, market_leveraged_sell_order, fee): """ 10 minute leveraged market trade on Kraken at 3x leverage Short trade @@ -373,7 +373,7 @@ def test_calc_close_trade_price(market_leveraged_buy_order, market_leveraged_sel @pytest.mark.usefixtures("init_persistence") -def test_update_limit_order(limit_leveraged_buy_order, limit_leveraged_sell_order, fee, caplog): +def test_update_limit_order_lev(limit_leveraged_buy_order, limit_leveraged_sell_order, fee, caplog): """ 10 minute leveraged limit trade on binance at 3x leverage @@ -444,7 +444,7 @@ def test_update_limit_order(limit_leveraged_buy_order, limit_leveraged_sell_orde @pytest.mark.usefixtures("init_persistence") -def test_update_market_order(market_leveraged_buy_order, market_leveraged_sell_order, fee, caplog): +def test_update_market_order_lev(market_leveraged_buy_order, market_leveraged_sell_order, fee, caplog): """ 10 minute leveraged market trade on Kraken at 3x leverage Short trade @@ -516,7 +516,7 @@ def test_update_market_order(market_leveraged_buy_order, market_leveraged_sell_o @pytest.mark.usefixtures("init_persistence") -def test_calc_close_trade_price_exception(limit_leveraged_buy_order, fee): +def test_calc_close_trade_price_exception_lev(limit_leveraged_buy_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, @@ -535,7 +535,7 @@ def test_calc_close_trade_price_exception(limit_leveraged_buy_order, fee): @pytest.mark.usefixtures("init_persistence") -def test_calc_profit(market_leveraged_buy_order, market_leveraged_sell_order, fee): +def test_calc_profit_lev(market_leveraged_buy_order, market_leveraged_sell_order, fee): """ # TODO: Update this one Leveraged trade on Kraken at 3x leverage diff --git a/tests/test_persistence_short.py b/tests/persistence/test_persistence_short.py similarity index 95% rename from tests/test_persistence_short.py rename to tests/persistence/test_persistence_short.py index 1f39f7439..e66914858 100644 --- a/tests/test_persistence_short.py +++ b/tests/persistence/test_persistence_short.py @@ -15,7 +15,7 @@ from tests.conftest import create_mock_trades_with_leverage, log_has, log_has_re @pytest.mark.usefixtures("init_persistence") -def test_interest_kraken(market_short_order, fee): +def test_interest_kraken_short(market_short_order, fee): """ Market trade on Kraken at 3x and 8x leverage Short trade @@ -80,7 +80,7 @@ def test_interest_kraken(market_short_order, fee): @ pytest.mark.usefixtures("init_persistence") -def test_interest_binance(market_short_order, fee): +def test_interest_binance_short(market_short_order, fee): """ Market trade on Binance at 3x and 5x leverage Short trade @@ -143,7 +143,7 @@ def test_interest_binance(market_short_order, fee): @ pytest.mark.usefixtures("init_persistence") -def test_calc_open_trade_value(market_short_order, fee): +def test_calc_open_trade_value_short(market_short_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, @@ -168,7 +168,7 @@ def test_calc_open_trade_value(market_short_order, fee): @ pytest.mark.usefixtures("init_persistence") -def test_update_open_order(limit_short_order): +def test_update_open_order_short(limit_short_order): trade = Trade( pair='ETH/BTC', stake_amount=1.00, @@ -193,7 +193,7 @@ def test_update_open_order(limit_short_order): @ pytest.mark.usefixtures("init_persistence") -def test_calc_close_trade_price_exception(limit_short_order, fee): +def test_calc_close_trade_price_exception_short(limit_short_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, @@ -213,7 +213,7 @@ def test_calc_close_trade_price_exception(limit_short_order, fee): @ pytest.mark.usefixtures("init_persistence") -def test_calc_close_trade_price(market_short_order, market_exit_short_order, fee): +def test_calc_close_trade_price_short(market_short_order, market_exit_short_order, fee): """ 10 minute short market trade on Kraken at 3x leverage Short trade @@ -257,7 +257,7 @@ def test_calc_close_trade_price(market_short_order, market_exit_short_order, fee @ pytest.mark.usefixtures("init_persistence") -def test_calc_open_close_trade_price(limit_short_order, limit_exit_short_order, fee): +def test_calc_open_close_trade_price_short(limit_short_order, limit_exit_short_order, fee): """ 5 hour short trade on Binance Short trade @@ -311,7 +311,7 @@ def test_calc_open_close_trade_price(limit_short_order, limit_exit_short_order, @ pytest.mark.usefixtures("init_persistence") -def test_trade_close(fee): +def test_trade_close_short(fee): """ Five hour short trade on Kraken at 3x leverage Short trade @@ -375,7 +375,7 @@ def test_trade_close(fee): @ pytest.mark.usefixtures("init_persistence") -def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, caplog): +def test_update_with_binance_short(limit_short_order, limit_exit_short_order, fee, caplog): """ 10 minute short limit trade on binance @@ -449,7 +449,7 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, cap @ pytest.mark.usefixtures("init_persistence") -def test_update_market_order( +def test_update_market_order_short( market_short_order, market_exit_short_order, fee, @@ -506,7 +506,6 @@ def test_update_market_order( assert trade.close_profit is None assert trade.close_date is None assert trade.interest_rate == 0.0005 - # TODO: Uncomment the next assert and make it work. # The logger also has the exact same but there's some spacing in there assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004173, open_since=.*\).", @@ -519,16 +518,16 @@ def test_update_market_order( assert trade.close_rate == 0.00004099 assert trade.close_profit == 0.03685505 assert trade.close_date is not None - # TODO: The amount should maybe be the opening amount + the interest - # TODO: Uncomment the next assert and make it work. + # TODO-mg: The amount should maybe be the opening amount + the interest + # TODO-mg: Uncomment the next assert and make it work. # The logger also has the exact same but there's some spacing in there - assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " - r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004099, open_since=.*\).", + assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, " + r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004173, open_since=.*\).", caplog) @ pytest.mark.usefixtures("init_persistence") -def test_calc_profit(market_short_order, market_exit_short_order, fee): +def test_calc_profit_short(market_short_order, market_exit_short_order, fee): """ Market trade on Kraken at 3x leverage Short trade @@ -624,7 +623,7 @@ def test_calc_profit(market_short_order, market_exit_short_order, fee): # assert trade.calc_profit_ratio(fee=0.003) == 0.06147824 -def test_adjust_stop_loss(fee): +def test_adjust_stop_loss_short(fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, @@ -650,23 +649,24 @@ def test_adjust_stop_loss(fee): assert trade.initial_stop_loss_pct == 0.05 # Get percent of profit with a custom rate (Higher than open rate) trade.adjust_stop_loss(0.7, 0.1) - assert round(trade.stop_loss, 8) == 1.17 # TODO-mg: What is this test? + # If the price goes down to 0.7, with a trailing stop of 0.1, the new stoploss at 0.1 above 0.7 would be 0.7*0.1 higher + assert round(trade.stop_loss, 8) == 0.77 assert trade.stop_loss_pct == 0.1 assert trade.initial_stop_loss == 1.05 assert trade.initial_stop_loss_pct == 0.05 # current rate lower again ... should not change trade.adjust_stop_loss(0.8, -0.1) - assert round(trade.stop_loss, 8) == 1.17 # TODO-mg: What is this test? + assert round(trade.stop_loss, 8) == 0.77 assert trade.initial_stop_loss == 1.05 assert trade.initial_stop_loss_pct == 0.05 # current rate higher... should raise stoploss trade.adjust_stop_loss(0.6, -0.1) - assert round(trade.stop_loss, 8) == 1.26 # TODO-mg: What is this test? + assert round(trade.stop_loss, 8) == 0.66 assert trade.initial_stop_loss == 1.05 assert trade.initial_stop_loss_pct == 0.05 # Initial is true but stop_loss set - so doesn't do anything trade.adjust_stop_loss(0.3, -0.1, True) - assert round(trade.stop_loss, 8) == 1.26 # TODO-mg: What is this test? + assert round(trade.stop_loss, 8) == 0.66 # TODO-mg: What is this test? assert trade.initial_stop_loss == 1.05 assert trade.initial_stop_loss_pct == 0.05 assert trade.stop_loss_pct == 0.1 @@ -677,7 +677,7 @@ def test_adjust_stop_loss(fee): @ pytest.mark.usefixtures("init_persistence") @ pytest.mark.parametrize('use_db', [True, False]) -def test_get_open(fee, use_db): +def test_get_open_short(fee, use_db): Trade.use_db = use_db Trade.reset_trades() create_mock_trades_with_leverage(fee, use_db) @@ -685,7 +685,7 @@ def test_get_open(fee, use_db): Trade.use_db = True -def test_stoploss_reinitialization(default_conf, fee): +def test_stoploss_reinitialization_short(default_conf, fee): # TODO-mg: I don't understand this at all, I was just going in the opposite direction as the matching function form test_persistance.py init_db(default_conf['db_url']) trade = Trade( @@ -743,7 +743,7 @@ def test_stoploss_reinitialization(default_conf, fee): @ pytest.mark.usefixtures("init_persistence") @ pytest.mark.parametrize('use_db', [True, False]) -def test_total_open_trades_stakes(fee, use_db): +def test_total_open_trades_stakes_short(fee, use_db): Trade.use_db = use_db Trade.reset_trades() res = Trade.total_open_trades_stakes() @@ -755,7 +755,7 @@ def test_total_open_trades_stakes(fee, use_db): @ pytest.mark.usefixtures("init_persistence") -def test_get_best_pair(fee): +def test_get_best_pair_short(fee): res = Trade.get_best_pair() assert res is None create_mock_trades_with_leverage(fee) From 2aa2b5bcfff98e81ad4d505cf712b09c41ab41cf Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 5 Jul 2021 23:53:49 -0600 Subject: [PATCH 26/46] Added checks for making sure stop_loss doesn't go below liquidation_price --- freqtrade/persistence/models.py | 27 ++++++++++++++++++- tests/conftest.py | 12 ++++++--- .../persistence/test_persistence_leverage.py | 4 +++ tests/persistence/test_persistence_short.py | 9 ++++++- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 54a5676d9..415024018 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -294,8 +294,30 @@ class LocalTrade(): def __init__(self, **kwargs): for key in kwargs: setattr(self, key, kwargs[key]) + self.set_liquidation_price(self.liquidation_price) self.recalc_open_trade_value() + def set_stop_loss_helper(self, stop_loss: Optional[float], liquidation_price: Optional[float]): + # Stoploss would be better as a computed variable, but that messes up the database so it might not be possible + # TODO-mg: What should be done about initial_stop_loss + if liquidation_price is not None: + if stop_loss is not None: + if self.is_short: + self.stop_loss = min(stop_loss, liquidation_price) + else: + self.stop_loss = max(stop_loss, liquidation_price) + else: + self.stop_loss = liquidation_price + self.liquidation_price = liquidation_price + else: + self.stop_loss = stop_loss + + def set_stop_loss(self, stop_loss: float): + self.set_stop_loss_helper(stop_loss=stop_loss, liquidation_price=self.liquidation_price) + + def set_liquidation_price(self, liquidation_price: float): + self.set_stop_loss_helper(stop_loss=self.stop_loss, liquidation_price=liquidation_price) + def __repr__(self): open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed' @@ -390,7 +412,7 @@ class LocalTrade(): def _set_new_stoploss(self, new_loss: float, stoploss: float): """Assign new stop value""" - self.stop_loss = new_loss + self.set_stop_loss(new_loss) if self.is_short: self.stop_loss_pct = abs(stoploss) else: @@ -484,6 +506,9 @@ class LocalTrade(): self.amount = float(safe_value_fallback(order, 'filled', 'amount')) if 'leverage' in order: self.leverage = order['leverage'] + if 'liquidation_price' in order: + self.liquidation_price = order['liquidation_price'] + self.set_stop_loss(self.stop_loss) self.recalc_open_trade_value() if self.is_open: payment = "SELL" if self.is_short else "BUY" diff --git a/tests/conftest.py b/tests/conftest.py index f935b7fa2..20fbde61c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2132,7 +2132,8 @@ def limit_short_order_open(): 'cost': 0.00106733393, 'remaining': 90.99181073, 'status': 'open', - 'is_short': True + 'is_short': True, + 'liquidation_price': 0.00001300 } @@ -2185,7 +2186,8 @@ def market_short_order(): 'remaining': 0.0, 'status': 'closed', 'is_short': True, - 'leverage': 3.0 + 'leverage': 3.0, + 'liquidation_price': 0.00004300 } @@ -2223,7 +2225,8 @@ def limit_leveraged_buy_order_open(): 'remaining': 272.97543219, 'leverage': 3.0, 'status': 'open', - 'exchange': 'binance' + 'exchange': 'binance', + 'liquidation_price': 0.00001000 } @@ -2277,7 +2280,8 @@ def market_leveraged_buy_order(): 'filled': 275.97543219, 'remaining': 0.0, 'status': 'closed', - 'exchange': 'kraken' + 'exchange': 'kraken', + 'liquidation_price': 0.00004000 } diff --git a/tests/persistence/test_persistence_leverage.py b/tests/persistence/test_persistence_leverage.py index 44da84f37..74103156d 100644 --- a/tests/persistence/test_persistence_leverage.py +++ b/tests/persistence/test_persistence_leverage.py @@ -428,6 +428,8 @@ def test_update_limit_order_lev(limit_leveraged_buy_order, limit_leveraged_sell_ assert trade.close_profit is None assert trade.close_date is None assert trade.borrowed == 0.0019999999998453998 + assert trade.stop_loss == 0.00001000 + assert trade.liquidation_price == 0.00001000 assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, " r"pair=ETH/BTC, amount=272.97543219, open_rate=0.00001099, open_since=.*\).", caplog) @@ -494,6 +496,8 @@ def test_update_market_order_lev(market_leveraged_buy_order, market_leveraged_se assert trade.close_profit is None assert trade.close_date is None assert trade.interest_rate == 0.0005 + assert trade.stop_loss == 0.00004000 + assert trade.liquidation_price == 0.00004000 # TODO: Uncomment the next assert and make it work. # The logger also has the exact same but there's some spacing in there assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, " diff --git a/tests/persistence/test_persistence_short.py b/tests/persistence/test_persistence_short.py index e66914858..67961f415 100644 --- a/tests/persistence/test_persistence_short.py +++ b/tests/persistence/test_persistence_short.py @@ -433,6 +433,8 @@ def test_update_with_binance_short(limit_short_order, limit_exit_short_order, fe assert trade.close_date is None assert trade.borrowed == 90.99181073 assert trade.is_short is True + assert trade.stop_loss == 0.00001300 + assert trade.liquidation_price == 0.00001300 assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, " r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001173, open_since=.*\).", caplog) @@ -506,6 +508,8 @@ def test_update_market_order_short( assert trade.close_profit is None assert trade.close_date is None assert trade.interest_rate == 0.0005 + assert trade.stop_loss == 0.00004300 + assert trade.liquidation_price == 0.00004300 # The logger also has the exact same but there's some spacing in there assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004173, open_since=.*\).", @@ -670,7 +674,10 @@ def test_adjust_stop_loss_short(fee): assert trade.initial_stop_loss == 1.05 assert trade.initial_stop_loss_pct == 0.05 assert trade.stop_loss_pct == 0.1 - trade.liquidation_price == 1.03 + trade.set_liquidation_price(0.63) + trade.adjust_stop_loss(0.59, -0.1) + assert trade.stop_loss == 0.63 + assert trade.liquidation_price == 0.63 # TODO-mg: Do a test with a trade that has a liquidation price From bb2a44735b8b833f2b2a9c32ecd7687c5ffca338 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 6 Jul 2021 00:11:43 -0600 Subject: [PATCH 27/46] Added liquidation_price check to test_stoploss_reinitialization_short --- tests/persistence/test_persistence_short.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/persistence/test_persistence_short.py b/tests/persistence/test_persistence_short.py index 67961f415..0d446c0a2 100644 --- a/tests/persistence/test_persistence_short.py +++ b/tests/persistence/test_persistence_short.py @@ -693,7 +693,6 @@ def test_get_open_short(fee, use_db): def test_stoploss_reinitialization_short(default_conf, fee): - # TODO-mg: I don't understand this at all, I was just going in the opposite direction as the matching function form test_persistance.py init_db(default_conf['db_url']) trade = Trade( pair='ETH/BTC', @@ -733,19 +732,24 @@ def test_stoploss_reinitialization_short(default_conf, fee): assert trade_adj.stop_loss_pct == 0.04 assert trade_adj.initial_stop_loss == 1.04 assert trade_adj.initial_stop_loss_pct == 0.04 - # Trailing stoploss (move stoplos up a bit) + # Trailing stoploss trade.adjust_stop_loss(0.98, -0.04) - assert trade_adj.stop_loss == 1.0208 + assert trade_adj.stop_loss == 1.0192 assert trade_adj.initial_stop_loss == 1.04 Trade.stoploss_reinitialization(-0.04) trades = Trade.get_open_trades() assert len(trades) == 1 trade_adj = trades[0] # Stoploss should not change in this case. - assert trade_adj.stop_loss == 1.0208 + assert trade_adj.stop_loss == 1.0192 assert trade_adj.stop_loss_pct == 0.04 assert trade_adj.initial_stop_loss == 1.04 assert trade_adj.initial_stop_loss_pct == 0.04 + # Stoploss can't go above liquidation price + trade_adj.set_liquidation_price(1.0) + trade.adjust_stop_loss(0.97, -0.04) + assert trade_adj.stop_loss == 1.0 + assert trade_adj.stop_loss == 1.0 @ pytest.mark.usefixtures("init_persistence") From 1414df5e27ea5886888a751919e794705015e06a Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 6 Jul 2021 00:18:03 -0600 Subject: [PATCH 28/46] updated timezone.utc time --- freqtrade/persistence/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 415024018..5254b7f4b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -615,7 +615,7 @@ class LocalTrade(): raise OperationalException(f"Leverage not available on {self.exchange} using freqtrade") open_date = self.open_date.replace(tzinfo=None) - now = (self.close_date or datetime.utcnow()).replace(tzinfo=None) + now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None) sec_per_hour = Decimal(3600) total_seconds = Decimal((now - open_date).total_seconds()) hours = total_seconds/sec_per_hour or zero From dd6cc1153bd52e0f7c9d5885f609cb43c091dff0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 6 Jul 2021 00:43:01 -0600 Subject: [PATCH 29/46] Tried to add liquidation price to order object, caused a test to fail --- freqtrade/persistence/migrations.py | 3 ++- freqtrade/persistence/models.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 69ffc544e..be503c42b 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -148,6 +148,7 @@ def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, col decl_base.metadata.create_all(engine) leverage = get_column_def(cols, 'leverage', '1.0') is_short = get_column_def(cols, 'is_short', 'False') + liquidation_price = get_column_def(cols, 'liquidation_price', 'False') with engine.begin() as connection: connection.execute(text(f""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, @@ -156,7 +157,7 @@ def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, col select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, null average, remaining, cost, order_date, order_filled_date, order_update_date, - {leverage} leverage, {is_short} is_short + {leverage} leverage, {is_short} is_short, {liquidation_price} liquidation_price from {table_back_name} """)) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 5254b7f4b..76a5bf34e 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -134,6 +134,7 @@ class Order(_DECL_BASE): leverage = Column(Float, nullable=True, default=1.0) is_short = Column(Boolean, nullable=True, default=False) + # liquidation_price = Column(Float, nullable=True) def __repr__(self): @@ -159,6 +160,7 @@ class Order(_DECL_BASE): self.remaining = order.get('remaining', self.remaining) self.cost = order.get('cost', self.cost) self.leverage = order.get('leverage', self.leverage) + # TODO-mg: liquidation price? is_short? if 'timestamp' in order and order['timestamp'] is not None: self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc) From 98acb0f4ff535e23209ff336ab142ac24ca06ff2 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 6 Jul 2021 22:34:08 -0600 Subject: [PATCH 30/46] set initial_stop_loss in stoploss helper --- freqtrade/persistence/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 76a5bf34e..97cb25e14 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -310,8 +310,11 @@ class LocalTrade(): self.stop_loss = max(stop_loss, liquidation_price) else: self.stop_loss = liquidation_price + self.initial_stop_loss = liquidation_price self.liquidation_price = liquidation_price else: + if not self.stop_loss: + self.initial_stop_loss = stop_loss self.stop_loss = stop_loss def set_stop_loss(self, stop_loss: float): From 150df3eb8803dd82a29cde6ebce4b4863d031b88 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 7 Jul 2021 00:43:06 -0600 Subject: [PATCH 31/46] Pass all but one test, because sqalchemy messes up --- tests/conftest_trades.py | 10 +++++----- tests/persistence/test_persistence_short.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 915cecd35..eeaa32792 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -395,7 +395,7 @@ def short_trade(fee): def leverage_order(): return { 'id': '1237', - 'symbol': 'ETC/BTC', + 'symbol': 'DOGE/BTC', 'status': 'closed', 'side': 'buy', 'type': 'limit', @@ -410,7 +410,7 @@ def leverage_order(): def leverage_order_sell(): return { 'id': '12368', - 'symbol': 'ETC/BTC', + 'symbol': 'DOGE/BTC', 'status': 'closed', 'side': 'sell', 'type': 'limit', @@ -452,7 +452,7 @@ def leverage_trade(fee): = 0.17524390243902502 """ trade = Trade( - pair='ETC/BTC', + pair='DOGE/BTC', stake_amount=15.129, amount=615.0, leverage=5.0, @@ -473,8 +473,8 @@ def leverage_trade(fee): close_date=datetime.now(tz=timezone.utc), interest_rate=0.0005 ) - o = Order.parse_from_ccxt_object(leverage_order(), 'ETC/BTC', 'sell') + o = Order.parse_from_ccxt_object(leverage_order(), 'DOGE/BTC', 'sell') trade.orders.append(o) - o = Order.parse_from_ccxt_object(leverage_order_sell(), 'ETC/BTC', 'sell') + o = Order.parse_from_ccxt_object(leverage_order_sell(), 'DOGE/BTC', 'sell') trade.orders.append(o) return trade diff --git a/tests/persistence/test_persistence_short.py b/tests/persistence/test_persistence_short.py index 0d446c0a2..11431c124 100644 --- a/tests/persistence/test_persistence_short.py +++ b/tests/persistence/test_persistence_short.py @@ -772,5 +772,5 @@ def test_get_best_pair_short(fee): create_mock_trades_with_leverage(fee) res = Trade.get_best_pair() assert len(res) == 2 - assert res[0] == 'ETC/BTC' + assert res[0] == 'DOGE/BTC' assert res[1] == 0.17524390243902502 From a19466c08578a0078622cc2b0c5356c4d304746a Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 7 Jul 2021 01:06:51 -0600 Subject: [PATCH 32/46] Moved leverage and is_short variables out of trade constructors and into conftest --- tests/conftest.py | 7 +++++-- tests/persistence/test_persistence_leverage.py | 3 --- tests/persistence/test_persistence_short.py | 2 -- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 20fbde61c..3923ab587 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2150,7 +2150,8 @@ def limit_exit_short_order_open(): 'amount': 90.99370639272354, 'filled': 0.0, 'remaining': 90.99370639272354, - 'status': 'open' + 'status': 'open', + 'leverage': 1.0 } @@ -2281,7 +2282,8 @@ def market_leveraged_buy_order(): 'remaining': 0.0, 'status': 'closed', 'exchange': 'kraken', - 'liquidation_price': 0.00004000 + 'liquidation_price': 0.00004000, + 'leverage': 3.0 } @@ -2298,5 +2300,6 @@ def market_leveraged_sell_order(): 'filled': 275.97543219, 'remaining': 0.0, 'status': 'closed', + 'leverage': 3.0, 'exchange': 'kraken' } diff --git a/tests/persistence/test_persistence_leverage.py b/tests/persistence/test_persistence_leverage.py index 74103156d..0453e5de5 100644 --- a/tests/persistence/test_persistence_leverage.py +++ b/tests/persistence/test_persistence_leverage.py @@ -157,7 +157,6 @@ def test_update_open_order_lev(limit_leveraged_buy_order): fee_open=0.1, fee_close=0.1, interest_rate=0.0005, - leverage=3.0, exchange='binance', interest_mode=InterestMode.HOURSPERDAY ) @@ -412,7 +411,6 @@ def test_update_limit_order_lev(limit_leveraged_buy_order, limit_leveraged_sell_ open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), fee_open=fee.return_value, fee_close=fee.return_value, - leverage=3.0, interest_rate=0.0005, exchange='binance', interest_mode=InterestMode.HOURSPERDAY @@ -480,7 +478,6 @@ def test_update_market_order_lev(market_leveraged_buy_order, market_leveraged_se amount=5, open_rate=0.00004099, is_open=True, - leverage=3, fee_open=fee.return_value, fee_close=fee.return_value, open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), diff --git a/tests/persistence/test_persistence_short.py b/tests/persistence/test_persistence_short.py index 11431c124..6a52eb91f 100644 --- a/tests/persistence/test_persistence_short.py +++ b/tests/persistence/test_persistence_short.py @@ -494,7 +494,6 @@ def test_update_market_order_short( fee_open=fee.return_value, fee_close=fee.return_value, open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), - leverage=3.0, interest_rate=0.0005, exchange='kraken', interest_mode=InterestMode.HOURSPER4 @@ -584,7 +583,6 @@ def test_calc_profit_short(market_short_order, market_exit_short_order, fee): fee_close=fee.return_value, exchange='kraken', is_short=True, - leverage=3.0, interest_rate=0.0005, interest_mode=InterestMode.HOURSPER4 ) From 86888dbbf018810894302a864f24aa08213aab4b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 7 Jul 2021 01:30:42 -0600 Subject: [PATCH 33/46] Took liquidation price out of order completely --- freqtrade/persistence/migrations.py | 3 +-- freqtrade/persistence/models.py | 4 ---- tests/conftest.py | 6 +----- tests/persistence/test_persistence_leverage.py | 4 ---- tests/persistence/test_persistence_short.py | 4 ---- 5 files changed, 2 insertions(+), 19 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index be503c42b..69ffc544e 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -148,7 +148,6 @@ def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, col decl_base.metadata.create_all(engine) leverage = get_column_def(cols, 'leverage', '1.0') is_short = get_column_def(cols, 'is_short', 'False') - liquidation_price = get_column_def(cols, 'liquidation_price', 'False') with engine.begin() as connection: connection.execute(text(f""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, @@ -157,7 +156,7 @@ def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, col select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, null average, remaining, cost, order_date, order_filled_date, order_update_date, - {leverage} leverage, {is_short} is_short, {liquidation_price} liquidation_price + {leverage} leverage, {is_short} is_short from {table_back_name} """)) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 97cb25e14..b9c1b89a8 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -134,7 +134,6 @@ class Order(_DECL_BASE): leverage = Column(Float, nullable=True, default=1.0) is_short = Column(Boolean, nullable=True, default=False) - # liquidation_price = Column(Float, nullable=True) def __repr__(self): @@ -511,9 +510,6 @@ class LocalTrade(): self.amount = float(safe_value_fallback(order, 'filled', 'amount')) if 'leverage' in order: self.leverage = order['leverage'] - if 'liquidation_price' in order: - self.liquidation_price = order['liquidation_price'] - self.set_stop_loss(self.stop_loss) self.recalc_open_trade_value() if self.is_open: payment = "SELL" if self.is_short else "BUY" diff --git a/tests/conftest.py b/tests/conftest.py index 3923ab587..f4877c46f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2132,8 +2132,7 @@ def limit_short_order_open(): 'cost': 0.00106733393, 'remaining': 90.99181073, 'status': 'open', - 'is_short': True, - 'liquidation_price': 0.00001300 + 'is_short': True } @@ -2188,7 +2187,6 @@ def market_short_order(): 'status': 'closed', 'is_short': True, 'leverage': 3.0, - 'liquidation_price': 0.00004300 } @@ -2227,7 +2225,6 @@ def limit_leveraged_buy_order_open(): 'leverage': 3.0, 'status': 'open', 'exchange': 'binance', - 'liquidation_price': 0.00001000 } @@ -2282,7 +2279,6 @@ def market_leveraged_buy_order(): 'remaining': 0.0, 'status': 'closed', 'exchange': 'kraken', - 'liquidation_price': 0.00004000, 'leverage': 3.0 } diff --git a/tests/persistence/test_persistence_leverage.py b/tests/persistence/test_persistence_leverage.py index 0453e5de5..286936ec4 100644 --- a/tests/persistence/test_persistence_leverage.py +++ b/tests/persistence/test_persistence_leverage.py @@ -426,8 +426,6 @@ def test_update_limit_order_lev(limit_leveraged_buy_order, limit_leveraged_sell_ assert trade.close_profit is None assert trade.close_date is None assert trade.borrowed == 0.0019999999998453998 - assert trade.stop_loss == 0.00001000 - assert trade.liquidation_price == 0.00001000 assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, " r"pair=ETH/BTC, amount=272.97543219, open_rate=0.00001099, open_since=.*\).", caplog) @@ -493,8 +491,6 @@ def test_update_market_order_lev(market_leveraged_buy_order, market_leveraged_se assert trade.close_profit is None assert trade.close_date is None assert trade.interest_rate == 0.0005 - assert trade.stop_loss == 0.00004000 - assert trade.liquidation_price == 0.00004000 # TODO: Uncomment the next assert and make it work. # The logger also has the exact same but there's some spacing in there assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, " diff --git a/tests/persistence/test_persistence_short.py b/tests/persistence/test_persistence_short.py index 6a52eb91f..3a9934c90 100644 --- a/tests/persistence/test_persistence_short.py +++ b/tests/persistence/test_persistence_short.py @@ -433,8 +433,6 @@ def test_update_with_binance_short(limit_short_order, limit_exit_short_order, fe assert trade.close_date is None assert trade.borrowed == 90.99181073 assert trade.is_short is True - assert trade.stop_loss == 0.00001300 - assert trade.liquidation_price == 0.00001300 assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, " r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001173, open_since=.*\).", caplog) @@ -507,8 +505,6 @@ def test_update_market_order_short( assert trade.close_profit is None assert trade.close_date is None assert trade.interest_rate == 0.0005 - assert trade.stop_loss == 0.00004300 - assert trade.liquidation_price == 0.00004300 # The logger also has the exact same but there's some spacing in there assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004173, open_since=.*\).", From a368dfa7b52066c0c5423f898823be18091cae84 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 7 Jul 2021 21:04:38 -0600 Subject: [PATCH 34/46] Changed InterestMode enum implementation --- freqtrade/enums/interestmode.py | 28 +++++++++++----------------- freqtrade/persistence/migrations.py | 6 ++++-- freqtrade/persistence/models.py | 4 +--- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/freqtrade/enums/interestmode.py b/freqtrade/enums/interestmode.py index c95f4731f..f35573f1f 100644 --- a/freqtrade/enums/interestmode.py +++ b/freqtrade/enums/interestmode.py @@ -1,30 +1,24 @@ from enum import Enum, auto from decimal import Decimal +from freqtrade.exceptions import OperationalException one = Decimal(1.0) four = Decimal(4.0) twenty_four = Decimal(24.0) -class FunctionProxy: - """Allow to mask a function as an Object.""" +class InterestMode(Enum): - def __init__(self, function): - self.function = function + HOURSPERDAY = "HOURSPERDAY" + HOURSPER4 = "HOURSPER4" # Hours per 4 hour segment def __call__(self, *args, **kwargs): - return self.function(*args, **kwargs) + borrowed, rate, hours = kwargs["borrowed"], kwargs["rate"], kwargs["hours"] -class InterestMode(Enum): - """Equations to calculate interest""" - - # Interest_rate is per day, minimum time of 1 hour - HOURSPERDAY = FunctionProxy( - lambda borrowed, rate, hours: borrowed * rate * max(hours, one)/twenty_four - ) - - # Interest_rate is per 4 hours, minimum time of 4 hours - HOURSPER4 = FunctionProxy( - lambda borrowed, rate, hours: borrowed * rate * (1 + max(0, (hours-four)/four)) - ) + if self.name == "HOURSPERDAY": + return borrowed * rate * max(hours, one)/twenty_four + elif self.name == "HOURSPER4": + return borrowed * rate * (1 + max(0, (hours-four)/four)) + else: + raise OperationalException(f"Leverage not available on this exchange with freqtrade") diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 69ffc544e..b7c969945 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -52,6 +52,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col interest_rate = get_column_def(cols, 'interest_rate', '0.0') liquidation_price = get_column_def(cols, 'liquidation_price', 'null') is_short = get_column_def(cols, 'is_short', 'False') + interest_mode = get_column_def(cols, 'interest_mode', 'null') # If ticker-interval existed use that, else null. if has_column(cols, 'ticker_interval'): timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') @@ -88,7 +89,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stoploss_order_id, stoploss_last_update, max_rate, min_rate, sell_reason, sell_order_status, strategy, timeframe, open_trade_value, close_profit_abs, - leverage, interest_rate, liquidation_price, is_short + leverage, interest_rate, liquidation_price, is_short, interest_mode ) select id, lower(exchange), case @@ -113,7 +114,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {strategy} strategy, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, {leverage} leverage, {interest_rate} interest_rate, - {liquidation_price} liquidation_price, {is_short} is_short + {liquidation_price} liquidation_price, {is_short} is_short, + {interest_mode} interest_mode from {table_back_name} """)) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b9c1b89a8..9aa340fdc 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -612,8 +612,6 @@ class LocalTrade(): # If nothing was borrowed if self.has_no_leverage: return zero - elif not self.interest_mode: - raise OperationalException(f"Leverage not available on {self.exchange} using freqtrade") open_date = self.open_date.replace(tzinfo=None) now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None) @@ -624,7 +622,7 @@ class LocalTrade(): rate = Decimal(interest_rate or self.interest_rate) borrowed = Decimal(self.borrowed) - return self.interest_mode.value(borrowed, rate, hours) + return self.interest_mode(borrowed=borrowed, rate=rate, hours=hours) def calc_close_trade_value(self, rate: Optional[float] = None, fee: Optional[float] = None, From 7f75c978a0382b6187f2d0ee4a54bb786e85e5eb Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 7 Jul 2021 21:14:08 -0600 Subject: [PATCH 35/46] All persistence margin tests pass Flake8 compliant, passed mypy, ran isort . --- freqtrade/enums/interestmode.py | 8 +- freqtrade/persistence/models.py | 44 ++++--- tests/conftest.py | 20 +-- tests/conftest_trades.py | 2 +- tests/persistence/test_persistence.py | 16 +-- .../persistence/test_persistence_leverage.py | 120 +++++++++--------- tests/persistence/test_persistence_short.py | 40 +++--- 7 files changed, 135 insertions(+), 115 deletions(-) diff --git a/freqtrade/enums/interestmode.py b/freqtrade/enums/interestmode.py index f35573f1f..f28193d9b 100644 --- a/freqtrade/enums/interestmode.py +++ b/freqtrade/enums/interestmode.py @@ -1,16 +1,20 @@ -from enum import Enum, auto from decimal import Decimal +from enum import Enum + from freqtrade.exceptions import OperationalException + one = Decimal(1.0) four = Decimal(4.0) twenty_four = Decimal(24.0) class InterestMode(Enum): + """Equations to calculate interest""" HOURSPERDAY = "HOURSPERDAY" HOURSPER4 = "HOURSPER4" # Hours per 4 hour segment + NONE = "NONE" def __call__(self, *args, **kwargs): @@ -21,4 +25,4 @@ class InterestMode(Enum): elif self.name == "HOURSPER4": return borrowed * rate * (1 + max(0, (hours-four)/four)) else: - raise OperationalException(f"Leverage not available on this exchange with freqtrade") + raise OperationalException("Leverage not available on this exchange with freqtrade") diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 9aa340fdc..050ae2c10 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -6,7 +6,7 @@ from datetime import datetime, timezone from decimal import Decimal from typing import Any, Dict, List, Optional -from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String, +from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String, create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.orm import Query, declarative_base, relationship, scoped_session, sessionmaker @@ -159,7 +159,7 @@ class Order(_DECL_BASE): self.remaining = order.get('remaining', self.remaining) self.cost = order.get('cost', self.cost) self.leverage = order.get('leverage', self.leverage) - # TODO-mg: liquidation price? is_short? + # TODO-mg: is_short? if 'timestamp' in order and order['timestamp'] is not None: self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc) @@ -269,7 +269,7 @@ class LocalTrade(): liquidation_price: Optional[float] = None is_short: bool = False leverage: float = 1.0 - interest_mode: Optional[InterestMode] = None + interest_mode: InterestMode = InterestMode.NONE @property def has_no_leverage(self) -> bool: @@ -299,8 +299,9 @@ class LocalTrade(): self.recalc_open_trade_value() def set_stop_loss_helper(self, stop_loss: Optional[float], liquidation_price: Optional[float]): - # Stoploss would be better as a computed variable, but that messes up the database so it might not be possible - # TODO-mg: What should be done about initial_stop_loss + # Stoploss would be better as a computed variable, + # but that messes up the database so it might not be possible + if liquidation_price is not None: if stop_loss is not None: if self.is_short: @@ -312,6 +313,8 @@ class LocalTrade(): self.initial_stop_loss = liquidation_price self.liquidation_price = liquidation_price else: + # programmming error check: 1 of liqudication_price or stop_loss must be set + assert stop_loss is not None if not self.stop_loss: self.initial_stop_loss = stop_loss self.stop_loss = stop_loss @@ -438,11 +441,13 @@ class LocalTrade(): if self.is_short: new_loss = float(current_price * (1 + abs(stoploss))) - if self.liquidation_price: # If trading on margin, don't set the stoploss below the liquidation price + # If trading on margin, don't set the stoploss below the liquidation price + if self.liquidation_price: new_loss = min(self.liquidation_price, new_loss) else: new_loss = float(current_price * (1 - abs(stoploss))) - if self.liquidation_price: # If trading on margin, don't set the stoploss below the liquidation price + # If trading on margin, don't set the stoploss below the liquidation price + if self.liquidation_price: new_loss = max(self.liquidation_price, new_loss) # no stop loss assigned yet @@ -457,8 +462,14 @@ class LocalTrade(): # evaluate if the stop loss needs to be updated else: - # stop losses only walk up, never down!, #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): + + higherStop = new_loss > self.stop_loss + lowerStop = new_loss < self.stop_loss + + # stop losses only walk up, never down!, + # ? But adding more to a margin account would create a lower liquidation price, + # ? decreasing the minimum stoploss + if (higherStop and not self.is_short) or (lowerStop and self.is_short): logger.debug(f"{self.pair} - Adjusting stoploss...") self._set_new_stoploss(new_loss, stoploss) else: @@ -518,10 +529,10 @@ class LocalTrade(): elif order_type in ('market', 'limit') and self.is_closing_trade(order['side']): if self.is_open: payment = "BUY" if self.is_short else "SELL" - # TODO-mg: On Shorts technically your buying a little bit more than the amount because it's the ammount plus the interest - # But this wll only print the original + # TODO-mg: On shorts, you buy a little bit more than the amount (amount + interest) + # This wll only print the original amount logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.') - self.close(safe_value_fallback(order, 'average', 'price')) # TODO: Double check this + self.close(safe_value_fallback(order, 'average', 'price')) # TODO-mg: Double check this elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): self.stoploss_order_id = None self.close_rate_requested = self.stop_loss @@ -644,7 +655,7 @@ class LocalTrade(): if self.is_short: amount = Decimal(self.amount) + Decimal(interest) else: - # The interest does not need to be purchased on longs because the user already owns that currency in your wallet + # Currency already owned for longs, no need to purchase amount = Decimal(self.amount) close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore @@ -697,11 +708,12 @@ class LocalTrade(): fee=(fee or self.fee_close), interest_rate=(interest_rate or self.interest_rate) ) - if (self.is_short and close_trade_value == 0.0) or (not self.is_short and self.open_trade_value == 0.0): + if ((self.is_short and close_trade_value == 0.0) or + (not self.is_short and self.open_trade_value == 0.0)): return 0.0 else: if self.has_no_leverage: - # TODO: This is only needed so that previous tests that included dummy stake_amounts don't fail. Undate those tests and get rid of this else + # TODO-mg: Use one profit_ratio calculation profit_ratio = (close_trade_value/self.open_trade_value) - 1 else: if self.is_short: @@ -864,7 +876,7 @@ class Trade(_DECL_BASE, LocalTrade): interest_rate = Column(Float, nullable=False, default=0.0) liquidation_price = Column(Float, nullable=True) is_short = Column(Boolean, nullable=False, default=False) - interest_mode = Column(String(100), nullable=True) + interest_mode = Column(Enum(InterestMode), nullable=True) # End of margin trading properties def __init__(self, **kwargs): diff --git a/tests/conftest.py b/tests/conftest.py index f4877c46f..eb0c14a45 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,8 +23,8 @@ from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker -from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, - mock_trade_5, mock_trade_6, short_trade, leverage_trade) +from tests.conftest_trades import (leverage_trade, mock_trade_1, mock_trade_2, mock_trade_3, + mock_trade_4, mock_trade_5, mock_trade_6, short_trade) logging.getLogger('').setLevel(logging.INFO) @@ -2209,7 +2209,7 @@ def market_exit_short_order(): # leverage 3x @pytest.fixture(scope='function') -def limit_leveraged_buy_order_open(): +def limit_lev_buy_order_open(): return { 'id': 'mocked_limit_buy', 'type': 'limit', @@ -2229,8 +2229,8 @@ def limit_leveraged_buy_order_open(): @pytest.fixture(scope='function') -def limit_leveraged_buy_order(limit_leveraged_buy_order_open): - order = deepcopy(limit_leveraged_buy_order_open) +def limit_lev_buy_order(limit_lev_buy_order_open): + order = deepcopy(limit_lev_buy_order_open) order['status'] = 'closed' order['filled'] = order['amount'] order['remaining'] = 0.0 @@ -2238,7 +2238,7 @@ def limit_leveraged_buy_order(limit_leveraged_buy_order_open): @pytest.fixture -def limit_leveraged_sell_order_open(): +def limit_lev_sell_order_open(): return { 'id': 'mocked_limit_sell', 'type': 'limit', @@ -2257,8 +2257,8 @@ def limit_leveraged_sell_order_open(): @pytest.fixture -def limit_leveraged_sell_order(limit_leveraged_sell_order_open): - order = deepcopy(limit_leveraged_sell_order_open) +def limit_lev_sell_order(limit_lev_sell_order_open): + order = deepcopy(limit_lev_sell_order_open) order['remaining'] = 0.0 order['filled'] = order['amount'] order['status'] = 'closed' @@ -2266,7 +2266,7 @@ def limit_leveraged_sell_order(limit_leveraged_sell_order_open): @pytest.fixture(scope='function') -def market_leveraged_buy_order(): +def market_lev_buy_order(): return { 'id': 'mocked_market_buy', 'type': 'market', @@ -2284,7 +2284,7 @@ def market_leveraged_buy_order(): @pytest.fixture -def market_leveraged_sell_order(): +def market_lev_sell_order(): return { 'id': 'mocked_limit_sell', 'type': 'market', diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index eeaa32792..e4290231c 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -444,7 +444,7 @@ def leverage_trade(fee): close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest = (615.0 * 0.128) - (615.0 * 0.128 * 0.0025) - 0.0378225 = 78.4853775 - total_profit = close_value - open_value + total_profit = close_value - open_value = 78.4853775 - 75.83411249999999 = 2.6512650000000093 total_profit_percentage = total_profit / stake_amount diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 68ebca3b1..9adb80b2a 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -79,10 +79,10 @@ def test_is_opening_closing_trade(fee): is_short=False, leverage=2.0 ) - assert trade.is_opening_trade('buy') == True - assert trade.is_opening_trade('sell') == False - assert trade.is_closing_trade('buy') == False - assert trade.is_closing_trade('sell') == True + assert trade.is_opening_trade('buy') is True + assert trade.is_opening_trade('sell') is False + assert trade.is_closing_trade('buy') is False + assert trade.is_closing_trade('sell') is True trade = Trade( id=2, @@ -99,10 +99,10 @@ def test_is_opening_closing_trade(fee): leverage=2.0 ) - assert trade.is_opening_trade('buy') == False - assert trade.is_opening_trade('sell') == True - assert trade.is_closing_trade('buy') == True - assert trade.is_closing_trade('sell') == False + assert trade.is_opening_trade('buy') is False + assert trade.is_opening_trade('sell') is True + assert trade.is_closing_trade('buy') is True + assert trade.is_closing_trade('sell') is False @pytest.mark.usefixtures("init_persistence") diff --git a/tests/persistence/test_persistence_leverage.py b/tests/persistence/test_persistence_leverage.py index 286936ec4..2326f92af 100644 --- a/tests/persistence/test_persistence_leverage.py +++ b/tests/persistence/test_persistence_leverage.py @@ -1,21 +1,15 @@ -import logging -from datetime import datetime, timedelta, timezone -from pathlib import Path -from types import FunctionType -from unittest.mock import MagicMock -import arrow -import pytest +from datetime import datetime, timedelta from math import isclose -from sqlalchemy import create_engine, inspect, text -from freqtrade import constants + +import pytest + from freqtrade.enums import InterestMode -from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db -from tests.conftest import create_mock_trades_with_leverage, log_has, log_has_re +from freqtrade.persistence import Trade +from tests.conftest import log_has_re @pytest.mark.usefixtures("init_persistence") -def test_interest_kraken_lev(market_leveraged_buy_order, fee): +def test_interest_kraken_lev(market_lev_buy_order, fee): """ Market trade on Kraken at 3x and 5x leverage Short trade @@ -54,10 +48,10 @@ def test_interest_kraken_lev(market_leveraged_buy_order, fee): interest_mode=InterestMode.HOURSPER4 ) - # The trades that last 10 minutes do not need to be rounded because they round up to 4 hours on kraken so we can predict the correct value + # 10 minutes round up to 4 hours evenly on kraken so we can predict the exact value assert float(trade.calculate_interest()) == 3.7707443218227e-06 trade.open_date = datetime.utcnow() - timedelta(hours=5, minutes=0) - # The trades that last for 5 hours have to be rounded because the length of time that the test takes will vary every time it runs, so we can't predict the exact value + # All trade > 5 hours will vary slightly due to execution time and interest calculated assert float(round(trade.calculate_interest(interest_rate=0.00025), 11) ) == round(2.3567152011391876e-06, 11) @@ -82,7 +76,7 @@ def test_interest_kraken_lev(market_leveraged_buy_order, fee): @pytest.mark.usefixtures("init_persistence") -def test_interest_binance_lev(market_leveraged_buy_order, fee): +def test_interest_binance_lev(market_lev_buy_order, fee): """ Market trade on Kraken at 3x and 5x leverage Short trade @@ -120,10 +114,10 @@ def test_interest_binance_lev(market_leveraged_buy_order, fee): interest_rate=0.0005, interest_mode=InterestMode.HOURSPERDAY ) - # The trades that last 10 minutes do not always need to be rounded because they round up to 4 hours on kraken so we can predict the correct value + # 10 minutes round up to 4 hours evenly on kraken so we can predict the them more accurately assert round(float(trade.calculate_interest()), 22) == round(4.166666666344583e-08, 22) trade.open_date = datetime.utcnow() - timedelta(hours=5, minutes=0) - # The trades that last for 5 hours have to be rounded because the length of time that the test takes will vary every time it runs, so we can't predict the exact value + # All trade > 5 hours will vary slightly due to execution time and interest calculated assert float(round(trade.calculate_interest(interest_rate=0.00025), 14) ) == round(1.0416666665861459e-07, 14) @@ -148,7 +142,7 @@ def test_interest_binance_lev(market_leveraged_buy_order, fee): @pytest.mark.usefixtures("init_persistence") -def test_update_open_order_lev(limit_leveraged_buy_order): +def test_update_open_order_lev(limit_lev_buy_order): trade = Trade( pair='ETH/BTC', stake_amount=1.00, @@ -163,15 +157,15 @@ def test_update_open_order_lev(limit_leveraged_buy_order): assert trade.open_order_id is None assert trade.close_profit is None assert trade.close_date is None - limit_leveraged_buy_order['status'] = 'open' - trade.update(limit_leveraged_buy_order) + limit_lev_buy_order['status'] = 'open' + trade.update(limit_lev_buy_order) assert trade.open_order_id is None assert trade.close_profit is None assert trade.close_date is None @pytest.mark.usefixtures("init_persistence") -def test_calc_open_trade_value_lev(market_leveraged_buy_order, fee): +def test_calc_open_trade_value_lev(market_lev_buy_order, fee): """ 10 minute leveraged market trade on Kraken at 3x leverage Short trade @@ -203,7 +197,7 @@ def test_calc_open_trade_value_lev(market_leveraged_buy_order, fee): interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'open_trade' - trade.update(market_leveraged_buy_order) # Buy @ 0.00001099 + trade.update(market_lev_buy_order) # Buy @ 0.00001099 # Get the open rate price with the standard fee rate assert trade._calc_open_trade_value() == 0.01134051354788177 trade.fee_open = 0.003 @@ -212,7 +206,7 @@ def test_calc_open_trade_value_lev(market_leveraged_buy_order, fee): @pytest.mark.usefixtures("init_persistence") -def test_calc_open_close_trade_price_lev(limit_leveraged_buy_order, limit_leveraged_sell_order, fee): +def test_calc_open_close_trade_price_lev(limit_lev_buy_order, limit_lev_sell_order, fee): """ 5 hour leveraged trade on Binance @@ -230,7 +224,9 @@ def test_calc_open_close_trade_price_lev(limit_leveraged_buy_order, limit_levera = (272.97543219 * 0.00001099) + (272.97543219 * 0.00001099 * 0.0025) = 0.0030074999997675204 close_value: ((amount_closed * close_rate) - (amount_closed * close_rate * fee)) - interest - = (272.97543219 * 0.00001173) - (272.97543219 * 0.00001173 * 0.0025) - 2.0833333331722917e-07 + = (272.97543219 * 0.00001173) + - (272.97543219 * 0.00001173 * 0.0025) + - 2.0833333331722917e-07 = 0.003193788481706411 total_profit = close_value - open_value = 0.003193788481706411 - 0.0030074999997675204 @@ -252,11 +248,11 @@ def test_calc_open_close_trade_price_lev(limit_leveraged_buy_order, limit_levera interest_mode=InterestMode.HOURSPERDAY ) trade.open_order_id = 'something' - trade.update(limit_leveraged_buy_order) + trade.update(limit_lev_buy_order) assert trade._calc_open_trade_value() == 0.00300749999976752 - trade.update(limit_leveraged_sell_order) + trade.update(limit_lev_sell_order) - # Will be slightly different due to slight changes in compilation time, and the fact that interest depends on time + # Is slightly different due to compilation time changes. Interest depends on time assert round(trade.calc_close_trade_value(), 11) == round(0.003193788481706411, 11) # Profit in BTC assert round(trade.calc_profit(), 8) == round(0.00018628848193889054, 8) @@ -281,11 +277,11 @@ def test_trade_close_lev(fee): open_value: (amount * open_rate) + (amount * open_rate * fee) = (15 * 0.1) + (15 * 0.1 * 0.0025) = 1.50375 - close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - interest + close_value: (amount * close_rate) + (amount * close_rate * fee) - interest = (15 * 0.2) - (15 * 0.2 * 0.0025) - 0.000625 = 2.9918750000000003 total_profit = close_value - open_value - = 2.9918750000000003 - 1.50375 + = 2.9918750000000003 - 1.50375 = 1.4881250000000001 total_profit_percentage = total_profit / stake_amount = 1.4881250000000001 / 0.5 @@ -324,7 +320,7 @@ def test_trade_close_lev(fee): @pytest.mark.usefixtures("init_persistence") -def test_calc_close_trade_price_lev(market_leveraged_buy_order, market_leveraged_sell_order, fee): +def test_calc_close_trade_price_lev(market_lev_buy_order, market_lev_sell_order, fee): """ 10 minute leveraged market trade on Kraken at 3x leverage Short trade @@ -337,15 +333,17 @@ def test_calc_close_trade_price_lev(market_leveraged_buy_order, market_leveraged borrowed: 0.0075414886436454 base time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) interest: borrowed * interest_rate * time-periods - = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto + = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto open_value: (amount * open_rate) + (amount * open_rate * fee) = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) = 0.01134051354788177 close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest - = (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.0025) - 3.7707443218227e-06 = 0.003393252246819716 - = (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.003) - 3.7707443218227e-06 = 0.003391549478403104 - = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.005) - 3.7707443218227e-06 = 0.011455101767040435 - + = (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.0025) - 3.7707443218227e-06 + = 0.003393252246819716 + = (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.003) - 3.7707443218227e-06 + = 0.003391549478403104 + = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.005) - 3.7707443218227e-06 + = 0.011455101767040435 """ trade = Trade( pair='ETH/BTC', @@ -361,18 +359,18 @@ def test_calc_close_trade_price_lev(market_leveraged_buy_order, market_leveraged interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'close_trade' - trade.update(market_leveraged_buy_order) # Buy @ 0.00001099 + trade.update(market_lev_buy_order) # Buy @ 0.00001099 # Get the close rate price with a custom close rate and a regular fee rate assert isclose(trade.calc_close_trade_value(rate=0.00001234), 0.003393252246819716) # Get the close rate price with a custom close rate and a custom fee rate assert isclose(trade.calc_close_trade_value(rate=0.00001234, fee=0.003), 0.003391549478403104) # Test when we apply a Sell order, and ask price with a custom fee rate - trade.update(market_leveraged_sell_order) + trade.update(market_lev_sell_order) assert isclose(trade.calc_close_trade_value(fee=0.005), 0.011455101767040435) @pytest.mark.usefixtures("init_persistence") -def test_update_limit_order_lev(limit_leveraged_buy_order, limit_leveraged_sell_order, fee, caplog): +def test_update_limit_order_lev(limit_lev_buy_order, limit_lev_sell_order, fee, caplog): """ 10 minute leveraged limit trade on binance at 3x leverage @@ -420,7 +418,7 @@ def test_update_limit_order_lev(limit_leveraged_buy_order, limit_leveraged_sell_ assert trade.close_date is None # trade.open_order_id = 'something' - trade.update(limit_leveraged_buy_order) + trade.update(limit_lev_buy_order) # assert trade.open_order_id is None assert trade.open_rate == 0.00001099 assert trade.close_profit is None @@ -431,7 +429,7 @@ def test_update_limit_order_lev(limit_leveraged_buy_order, limit_leveraged_sell_ caplog) caplog.clear() # trade.open_order_id = 'something' - trade.update(limit_leveraged_sell_order) + trade.update(limit_lev_sell_order) # assert trade.open_order_id is None assert trade.close_rate == 0.00001173 assert trade.close_profit == round(0.18645514861995735, 8) @@ -442,7 +440,7 @@ def test_update_limit_order_lev(limit_leveraged_buy_order, limit_leveraged_sell_ @pytest.mark.usefixtures("init_persistence") -def test_update_market_order_lev(market_leveraged_buy_order, market_leveraged_sell_order, fee, caplog): +def test_update_market_order_lev(market_lev_buy_order, market_lev_sell_order, fee, caplog): """ 10 minute leveraged market trade on Kraken at 3x leverage Short trade @@ -484,7 +482,7 @@ def test_update_market_order_lev(market_leveraged_buy_order, market_leveraged_se interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'something' - trade.update(market_leveraged_buy_order) + trade.update(market_lev_buy_order) assert trade.leverage == 3.0 assert trade.open_order_id is None assert trade.open_rate == 0.00004099 @@ -499,7 +497,7 @@ def test_update_market_order_lev(market_leveraged_buy_order, market_leveraged_se caplog.clear() trade.is_open = True trade.open_order_id = 'something' - trade.update(market_leveraged_sell_order) + trade.update(market_lev_sell_order) assert trade.open_order_id is None assert trade.close_rate == 0.00004173 assert trade.close_profit == round(0.03802415223225211, 8) @@ -513,7 +511,7 @@ def test_update_market_order_lev(market_leveraged_buy_order, market_leveraged_se @pytest.mark.usefixtures("init_persistence") -def test_calc_close_trade_price_exception_lev(limit_leveraged_buy_order, fee): +def test_calc_close_trade_price_exception_lev(limit_lev_buy_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, @@ -527,14 +525,13 @@ def test_calc_close_trade_price_exception_lev(limit_leveraged_buy_order, fee): interest_mode=InterestMode.HOURSPERDAY ) trade.open_order_id = 'something' - trade.update(limit_leveraged_buy_order) + trade.update(limit_lev_buy_order) assert trade.calc_close_trade_value() == 0.0 @pytest.mark.usefixtures("init_persistence") -def test_calc_profit_lev(market_leveraged_buy_order, market_leveraged_sell_order, fee): +def test_calc_profit_lev(market_lev_buy_order, market_lev_sell_order, fee): """ - # TODO: Update this one Leveraged trade on Kraken at 3x leverage fee: 0.25% base or 0.3% interest_rate: 0.05%, 0.25% per 4 hrs @@ -547,17 +544,22 @@ def test_calc_profit_lev(market_leveraged_buy_order, market_leveraged_sell_order 5 hours = 5/4 interest: borrowed * interest_rate * time-periods - = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto - = 0.0075414886436454 * 0.00025 * 5/4 = 2.3567152011391876e-06 crypto - = 0.0075414886436454 * 0.0005 * 5/4 = 4.713430402278375e-06 crypto - = 0.0075414886436454 * 0.00025 * 1 = 1.88537216091135e-06 crypto + = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto + = 0.0075414886436454 * 0.00025 * 5/4 = 2.3567152011391876e-06 crypto + = 0.0075414886436454 * 0.0005 * 5/4 = 4.713430402278375e-06 crypto + = 0.0075414886436454 * 0.00025 * 1 = 1.88537216091135e-06 crypto open_value: (amount * open_rate) + (amount * open_rate * fee) - = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) = 0.01134051354788177 + = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) + = 0.01134051354788177 close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest - (275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.0025) - 3.7707443218227e-06 = 0.01479007168225405 - (275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.0025) - 2.3567152011391876e-06 = 0.001200640891872485 - (275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.003) - 4.713430402278375e-06 = 0.014781713536310649 - (275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.003) - 1.88537216091135e-06 = 0.0012005092285933775 + (275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.0025) - 3.7707443218227e-06 + = 0.01479007168225405 + (275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.0025) - 2.3567152011391876e-06 + = 0.001200640891872485 + (275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.003) - 4.713430402278375e-06 + = 0.014781713536310649 + (275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.003) - 1.88537216091135e-06 + = 0.0012005092285933775 total_profit = close_value - open_value = 0.01479007168225405 - 0.01134051354788177 = 0.003449558134372281 = 0.001200640891872485 - 0.01134051354788177 = -0.010139872656009285 @@ -584,7 +586,7 @@ def test_calc_profit_lev(market_leveraged_buy_order, market_leveraged_sell_order interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'something' - trade.update(market_leveraged_buy_order) # Buy @ 0.00001099 + trade.update(market_lev_buy_order) # Buy @ 0.00001099 # Custom closing rate and regular fee rate # Higher than open rate @@ -615,7 +617,7 @@ def test_calc_profit_lev(market_leveraged_buy_order, market_leveraged_sell_order interest_rate=0.00025) == round(-2.6891253964381554, 8) # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 - trade.update(market_leveraged_sell_order) + trade.update(market_lev_sell_order) assert trade.calc_profit() == round(0.0001433793561218866, 8) assert trade.calc_profit_ratio() == round(0.03802415223225211, 8) diff --git a/tests/persistence/test_persistence_short.py b/tests/persistence/test_persistence_short.py index 3a9934c90..ba08e1632 100644 --- a/tests/persistence/test_persistence_short.py +++ b/tests/persistence/test_persistence_short.py @@ -1,17 +1,12 @@ -import logging -from datetime import datetime, timedelta, timezone -from pathlib import Path -from types import FunctionType -from unittest.mock import MagicMock +from datetime import datetime, timedelta +from math import isclose + import arrow import pytest -from math import isclose -from sqlalchemy import create_engine, inspect, text -from freqtrade import constants + from freqtrade.enums import InterestMode -from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db -from tests.conftest import create_mock_trades_with_leverage, log_has, log_has_re +from freqtrade.persistence import Trade, init_db +from tests.conftest import create_mock_trades_with_leverage, log_has_re @pytest.mark.usefixtures("init_persistence") @@ -302,11 +297,12 @@ def test_calc_open_close_trade_price_short(limit_short_order, limit_exit_short_o assert trade._calc_open_trade_value() == 0.0010646656050132426 trade.update(limit_exit_short_order) - # Will be slightly different due to slight changes in compilation time, and the fact that interest depends on time + # Is slightly different due to compilation time. Interest depends on time assert round(trade.calc_close_trade_value(), 11) == round(0.001002604427005832, 11) # Profit in BTC assert round(trade.calc_profit(), 8) == round(0.00006206117800741065, 8) # Profit in percent + # TODO-mg get this working # assert round(trade.calc_profit_ratio(), 11) == round(0.05822425142973869, 11) @@ -499,7 +495,7 @@ def test_update_market_order_short( trade.open_order_id = 'something' trade.update(market_short_order) assert trade.leverage == 3.0 - assert trade.is_short == True + assert trade.is_short is True assert trade.open_order_id is None assert trade.open_rate == 0.00004173 assert trade.close_profit is None @@ -546,17 +542,22 @@ def test_calc_profit_short(market_short_order, market_exit_short_order, fee): = 275.97543219 * 0.0005 * 5/4 = 0.17248464511875 crypto = 275.97543219 * 0.00025 * 1 = 0.0689938580475 crypto open_value: (amount * open_rate) - (amount * open_rate * fee) - = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.0025) = 0.011487663648325479 + = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.0025) + = 0.011487663648325479 amount_closed: amount + interest = 275.97543219 + 0.137987716095 = 276.113419906095 = 275.97543219 + 0.086242322559375 = 276.06167451255936 = 275.97543219 + 0.17248464511875 = 276.14791683511874 = 275.97543219 + 0.0689938580475 = 276.0444260480475 close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - (276.113419906095 * 0.00004374) + (276.113419906095 * 0.00004374 * 0.0025) = 0.012107393989159325 - (276.06167451255936 * 0.00000437) + (276.06167451255936 * 0.00000437 * 0.0025) = 0.0012094054914139338 - (276.14791683511874 * 0.00004374) + (276.14791683511874 * 0.00004374 * 0.003) = 0.012114946012015198 - (276.0444260480475 * 0.00000437) + (276.0444260480475 * 0.00000437 * 0.003) = 0.0012099330842554573 + (276.113419906095 * 0.00004374) + (276.113419906095 * 0.00004374 * 0.0025) + = 0.012107393989159325 + (276.06167451255936 * 0.00000437) + (276.06167451255936 * 0.00000437 * 0.0025) + = 0.0012094054914139338 + (276.14791683511874 * 0.00004374) + (276.14791683511874 * 0.00004374 * 0.003) + = 0.012114946012015198 + (276.0444260480475 * 0.00000437) + (276.0444260480475 * 0.00000437 * 0.003) + = 0.0012099330842554573 total_profit = open_value - close_value = print(0.011487663648325479 - 0.012107393989159325) = -0.0006197303408338461 = print(0.011487663648325479 - 0.0012094054914139338) = 0.010278258156911545 @@ -647,7 +648,8 @@ def test_adjust_stop_loss_short(fee): assert trade.initial_stop_loss_pct == 0.05 # Get percent of profit with a custom rate (Higher than open rate) trade.adjust_stop_loss(0.7, 0.1) - # If the price goes down to 0.7, with a trailing stop of 0.1, the new stoploss at 0.1 above 0.7 would be 0.7*0.1 higher + # If the price goes down to 0.7, with a trailing stop of 0.1, + # the new stoploss at 0.1 above 0.7 would be 0.7*0.1 higher assert round(trade.stop_loss, 8) == 0.77 assert trade.stop_loss_pct == 0.1 assert trade.initial_stop_loss == 1.05 From 546a7353dfd62e91c677b34c3eab731186b35ce8 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 8 Jul 2021 00:33:40 -0600 Subject: [PATCH 36/46] Added docstrings to methods --- freqtrade/persistence/migrations.py | 1 + freqtrade/persistence/models.py | 29 ++++++++++++++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index b7c969945..c3b07d1b1 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -150,6 +150,7 @@ def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, col decl_base.metadata.create_all(engine) leverage = get_column_def(cols, 'leverage', '1.0') is_short = get_column_def(cols, 'is_short', 'False') + # TODO-mg: Should liquidation price go in here? with engine.begin() as connection: connection.execute(text(f""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 050ae2c10..17318a615 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -273,10 +273,16 @@ class LocalTrade(): @property def has_no_leverage(self) -> bool: + """Returns true if this is a non-leverage, non-short trade""" return (self.leverage == 1.0 and not self.is_short) or self.leverage is None @property def borrowed(self) -> float: + """ + 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 + """ if self.has_no_leverage: return 0.0 elif not self.is_short: @@ -299,6 +305,7 @@ class LocalTrade(): self.recalc_open_trade_value() def set_stop_loss_helper(self, stop_loss: Optional[float], liquidation_price: Optional[float]): + """Helper function for set_liquidation_price and set_stop_loss""" # Stoploss would be better as a computed variable, # but that messes up the database so it might not be possible @@ -320,9 +327,17 @@ class LocalTrade(): self.stop_loss = stop_loss def set_stop_loss(self, stop_loss: float): + """ + Method you should use to set self.stop_loss. + Assures stop_loss is not passed the liquidation price + """ self.set_stop_loss_helper(stop_loss=stop_loss, liquidation_price=self.liquidation_price) def set_liquidation_price(self, liquidation_price: float): + """ + Method you should use to set self.liquidation price. + Assures stop_loss is not passed the liquidation price + """ self.set_stop_loss_helper(stop_loss=self.stop_loss, liquidation_price=liquidation_price) def __repr__(self): @@ -463,13 +478,13 @@ class LocalTrade(): # evaluate if the stop loss needs to be updated else: - higherStop = new_loss > self.stop_loss - lowerStop = new_loss < self.stop_loss + higher_stop = new_loss > self.stop_loss + lower_stop = new_loss < self.stop_loss # stop losses only walk up, never down!, # ? But adding more to a margin account would create a lower liquidation price, # ? decreasing the minimum stoploss - if (higherStop and not self.is_short) or (lowerStop and self.is_short): + if (higher_stop and not self.is_short) or (lower_stop and self.is_short): logger.debug(f"{self.pair} - Adjusting stoploss...") self._set_new_stoploss(new_loss, stoploss) else: @@ -601,7 +616,7 @@ class LocalTrade(): """ open_trade = Decimal(self.amount) * Decimal(self.open_rate) fees = open_trade * Decimal(self.fee_open) - if (self.is_short): + if self.is_short: return float(open_trade - fees) else: return float(open_trade + fees) @@ -661,7 +676,7 @@ class LocalTrade(): 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): + if self.is_short: return float(close_trade + fees) else: return float(close_trade - fees - interest) @@ -866,8 +881,8 @@ class Trade(_DECL_BASE, LocalTrade): max_rate = Column(Float, nullable=True, default=0.0) # Lowest price reached min_rate = Column(Float, nullable=True) - sell_reason = Column(String(100), nullable=True) # TODO: Change to close_reason - sell_order_status = Column(String(100), nullable=True) # TODO: Change to close_order_status + sell_reason = Column(String(100), nullable=True) # TODO-mg: Change to close_reason + sell_order_status = Column(String(100), nullable=True) # TODO-mg: Change to close_order_status strategy = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) From 358f0303b90e066b4c744e344f4153c55a890121 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 8 Jul 2021 05:37:54 -0600 Subject: [PATCH 37/46] updated ratio_calc_profit function --- freqtrade/persistence/models.py | 32 ++++++--- .../persistence/test_persistence_leverage.py | 67 ++++++++++++------- 2 files changed, 62 insertions(+), 37 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 17318a615..a2ca7badb 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -609,12 +609,14 @@ class LocalTrade(): def update_order(self, order: Dict) -> None: Order.update_orders(self.orders, order) - def _calc_open_trade_value(self) -> float: + def _calc_open_trade_value(self, amount: Optional[float] = None) -> float: """ Calculate the open_rate including open_fee. :return: Price in of the open trade incl. Fees """ - open_trade = Decimal(self.amount) * Decimal(self.open_rate) + if amount is None: + amount = self.amount + open_trade = Decimal(amount) * Decimal(self.open_rate) fees = open_trade * Decimal(self.fee_open) if self.is_short: return float(open_trade - fees) @@ -651,6 +653,7 @@ class LocalTrade(): return self.interest_mode(borrowed=borrowed, rate=rate, hours=hours) def calc_close_trade_value(self, rate: Optional[float] = None, + fee: Optional[float] = None, interest_rate: Optional[float] = None) -> float: """ @@ -718,23 +721,30 @@ class LocalTrade(): If interest_rate is not set self.interest_rate will be used :return: profit ratio as float """ + close_trade_value = self.calc_close_trade_value( rate=(rate or self.close_rate), fee=(fee or self.fee_close), interest_rate=(interest_rate or self.interest_rate) ) - if ((self.is_short and close_trade_value == 0.0) or - (not self.is_short and self.open_trade_value == 0.0)): + + if self.leverage is None: + leverage = 1.0 + else: + leverage = self.leverage + + stake_value = self._calc_open_trade_value(amount=(self.amount/leverage)) + + 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) + + if (short_close_zero or long_close_zero): return 0.0 else: - if self.has_no_leverage: - # TODO-mg: Use one profit_ratio calculation - profit_ratio = (close_trade_value/self.open_trade_value) - 1 + if self.is_short: + profit_ratio = ((self.open_trade_value - close_trade_value) / stake_value) else: - if self.is_short: - profit_ratio = ((self.open_trade_value - close_trade_value) / self.stake_amount) - else: - profit_ratio = ((close_trade_value - self.open_trade_value) / self.stake_amount) + profit_ratio = ((close_trade_value - self.open_trade_value) / stake_value) return float(f"{profit_ratio:.8f}") diff --git a/tests/persistence/test_persistence_leverage.py b/tests/persistence/test_persistence_leverage.py index 2326f92af..d2345163d 100644 --- a/tests/persistence/test_persistence_leverage.py +++ b/tests/persistence/test_persistence_leverage.py @@ -228,12 +228,15 @@ def test_calc_open_close_trade_price_lev(limit_lev_buy_order, limit_lev_sell_ord - (272.97543219 * 0.00001173 * 0.0025) - 2.0833333331722917e-07 = 0.003193788481706411 + stake_value: (amount/lev * open_rate) + (amount/lev * open_rate * fee) + = (272.97543219/3 * 0.00001099) + (272.97543219/3 * 0.00001099 * 0.0025) + = 0.0010024999999225066 total_profit = close_value - open_value = 0.003193788481706411 - 0.0030074999997675204 = 0.00018628848193889044 - total_profit_percentage = total_profit / stake_amount - = 0.00018628848193889054 / 0.0009999999999226999 - = 0.18628848195329067 + total_profit_percentage = total_profit / stake_value + = 0.00018628848193889054 / 0.0010024999999225066 + = 0.18582392214792087 """ trade = Trade( pair='ETH/BTC', @@ -257,7 +260,7 @@ def test_calc_open_close_trade_price_lev(limit_lev_buy_order, limit_lev_sell_ord # Profit in BTC assert round(trade.calc_profit(), 8) == round(0.00018628848193889054, 8) # Profit in percent - assert round(trade.calc_profit_ratio(), 8) == round(0.18628848195329067, 8) + assert round(trade.calc_profit_ratio(), 8) == round(0.18582392214792087, 8) @pytest.mark.usefixtures("init_persistence") @@ -280,12 +283,15 @@ def test_trade_close_lev(fee): close_value: (amount * close_rate) + (amount * close_rate * fee) - interest = (15 * 0.2) - (15 * 0.2 * 0.0025) - 0.000625 = 2.9918750000000003 + stake_value: (amount/lev * open_rate) + (amount/lev * open_rate * fee) + = ((15/3) * 0.1) + ((15/3) * 0.1 * 0.0025) + = 0.50125 total_profit = close_value - open_value = 2.9918750000000003 - 1.50375 = 1.4881250000000001 - total_profit_percentage = total_profit / stake_amount - = 1.4881250000000001 / 0.5 - = 2.9762500000000003 + total_profit_ratio = total_profit / stake_value + = 1.4881250000000001 / 0.50125 + = 2.968827930174564 """ trade = Trade( pair='ETH/BTC', @@ -306,7 +312,7 @@ def test_trade_close_lev(fee): assert trade.is_open is True trade.close(0.2) assert trade.is_open is False - assert trade.close_profit == round(2.9762500000000003, 8) + assert trade.close_profit == round(2.968827930174564, 8) assert trade.close_date is not None # TODO-mg: Remove these comments probably @@ -391,12 +397,15 @@ def test_update_limit_order_lev(limit_lev_buy_order, limit_lev_sell_order, fee, close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) = (272.97543219 * 0.00001173) - (272.97543219 * 0.00001173 * 0.0025) = 0.003193996815039728 + stake_value: (amount/lev * open_rate) + (amount/lev * open_rate * fee) + = (272.97543219/3 * 0.00001099) + (272.97543219/3 * 0.00001099 * 0.0025) + = 0.0010024999999225066 total_profit = close_value - open_value - interest = 0.003193996815039728 - 0.0030074999997675204 - 4.166666666344583e-08 = 0.00018645514860554435 - total_profit_percentage = total_profit / stake_amount - = 0.00018645514860554435 / 0.0009999999999226999 - = 0.18645514861995735 + total_profit_percentage = total_profit / stake_value + = 0.00018645514860554435 / 0.0010024999999225066 + = 0.1859901731869899 """ trade = Trade( @@ -432,7 +441,7 @@ def test_update_limit_order_lev(limit_lev_buy_order, limit_lev_sell_order, fee, trade.update(limit_lev_sell_order) # assert trade.open_order_id is None assert trade.close_rate == 0.00001173 - assert trade.close_profit == round(0.18645514861995735, 8) + assert trade.close_profit == round(0.1859901731869899, 8) assert trade.close_date is not None assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, " r"pair=ETH/BTC, amount=272.97543219, open_rate=0.00001099, open_since=.*\).", @@ -460,12 +469,15 @@ def test_update_market_order_lev(market_lev_buy_order, market_lev_sell_order, fe close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.0025) = 0.011487663648325479 + stake_value: (amount/lev * open_rate) + (amount/lev * open_rate * fee) + = (275.97543219/3 * 0.00004099) + (275.97543219/3 * 0.00004099 * 0.0025) + = 0.0037801711826272568 total_profit = close_value - open_value - interest = 0.011487663648325479 - 0.01134051354788177 - 3.7707443218227e-06 = 0.0001433793561218866 - total_profit_percentage = total_profit / stake_amount - = 0.0001433793561218866 / 0.0037707443218227 - = 0.03802415223225211 + total_profit_percentage = total_profit / stake_value + = 0.0001433793561218866 / 0.0037801711826272568 + = 0.03792932890997717 """ trade = Trade( id=1, @@ -500,7 +512,7 @@ def test_update_market_order_lev(market_lev_buy_order, market_lev_sell_order, fe trade.update(market_lev_sell_order) assert trade.open_order_id is None assert trade.close_rate == 0.00004173 - assert trade.close_profit == round(0.03802415223225211, 8) + assert trade.close_profit == round(0.03792932890997717, 8) assert trade.close_date is not None # TODO: The amount should maybe be the opening amount + the interest # TODO: Uncomment the next assert and make it work. @@ -560,16 +572,19 @@ def test_calc_profit_lev(market_lev_buy_order, market_lev_sell_order, fee): = 0.014781713536310649 (275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.003) - 1.88537216091135e-06 = 0.0012005092285933775 + stake_value: (amount/lev * open_rate) + (amount/lev * open_rate * fee) + = (275.97543219/3 * 0.00004099) + (275.97543219/3 * 0.00004099 * 0.0025) + = 0.0037801711826272568 total_profit = close_value - open_value = 0.01479007168225405 - 0.01134051354788177 = 0.003449558134372281 = 0.001200640891872485 - 0.01134051354788177 = -0.010139872656009285 = 0.014781713536310649 - 0.01134051354788177 = 0.0034411999884288794 = 0.0012005092285933775 - 0.01134051354788177 = -0.010140004319288392 - total_profit_percentage = total_profit / stake_amount - 0.003449558134372281/0.0037707443218227 = 0.9148215418394732 - -0.010139872656009285/0.0037707443218227 = -2.6890904793852157 - 0.0034411999884288794/0.0037707443218227 = 0.9126049646255184 - -0.010140004319288392/0.0037707443218227 = -2.6891253964381554 + total_profit_percentage = total_profit / stake_value + 0.003449558134372281/0.0037801711826272568 = 0.9125401913610705 + -0.010139872656009285/0.0037801711826272568 = -2.682384518089991 + 0.0034411999884288794/0.0037801711826272568 = 0.9103291417710906 + -0.010140004319288392/0.0037801711826272568 = -2.6824193480679854 """ trade = Trade( @@ -593,33 +608,33 @@ def test_calc_profit_lev(market_lev_buy_order, market_lev_sell_order, fee): assert trade.calc_profit(rate=0.00005374, interest_rate=0.0005) == round( 0.003449558134372281, 8) assert trade.calc_profit_ratio( - rate=0.00005374, interest_rate=0.0005) == round(0.9148215418394732, 8) + rate=0.00005374, interest_rate=0.0005) == round(0.9125401913610705, 8) # Lower than open rate trade.open_date = datetime.utcnow() - timedelta(hours=5, minutes=0) assert trade.calc_profit( rate=0.00000437, interest_rate=0.00025) == round(-0.010139872656009285, 8) assert trade.calc_profit_ratio( - rate=0.00000437, interest_rate=0.00025) == round(-2.6890904793852157, 8) + rate=0.00000437, interest_rate=0.00025) == round(-2.682384518089991, 8) # Custom closing rate and custom fee rate # Higher than open rate assert trade.calc_profit(rate=0.00005374, fee=0.003, interest_rate=0.0005) == round(0.0034411999884288794, 8) assert trade.calc_profit_ratio(rate=0.00005374, fee=0.003, - interest_rate=0.0005) == round(0.9126049646255184, 8) + interest_rate=0.0005) == round(0.9103291417710906, 8) # Lower than open rate trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10) assert trade.calc_profit(rate=0.00000437, fee=0.003, interest_rate=0.00025) == round(-0.010140004319288392, 8) assert trade.calc_profit_ratio(rate=0.00000437, fee=0.003, - interest_rate=0.00025) == round(-2.6891253964381554, 8) + interest_rate=0.00025) == round(-2.6824193480679854, 8) # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 trade.update(market_lev_sell_order) assert trade.calc_profit() == round(0.0001433793561218866, 8) - assert trade.calc_profit_ratio() == round(0.03802415223225211, 8) + assert trade.calc_profit_ratio() == round(0.03792932890997717, 8) # Test with a custom fee rate on the close trade # assert trade.calc_profit(fee=0.003) == 0.00006163 From f1dc6b54adf78a69e975b93f0984f67b099b50ec Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 10 Jul 2021 20:44:57 -0600 Subject: [PATCH 38/46] Updated interest and ratio calculations to correct functions --- freqtrade/enums/interestmode.py | 6 +- freqtrade/persistence/models.py | 20 +- tests/conftest_trades.py | 24 +-- tests/persistence/test_persistence.py | 1 - .../persistence/test_persistence_leverage.py | 177 ++++++++-------- tests/persistence/test_persistence_short.py | 192 +++++++++--------- 6 files changed, 208 insertions(+), 212 deletions(-) diff --git a/freqtrade/enums/interestmode.py b/freqtrade/enums/interestmode.py index f28193d9b..4128fc7a0 100644 --- a/freqtrade/enums/interestmode.py +++ b/freqtrade/enums/interestmode.py @@ -1,5 +1,6 @@ from decimal import Decimal from enum import Enum +from math import ceil from freqtrade.exceptions import OperationalException @@ -21,8 +22,9 @@ class InterestMode(Enum): borrowed, rate, hours = kwargs["borrowed"], kwargs["rate"], kwargs["hours"] if self.name == "HOURSPERDAY": - return borrowed * rate * max(hours, one)/twenty_four + return borrowed * rate * ceil(hours)/twenty_four elif self.name == "HOURSPER4": - return borrowed * rate * (1 + max(0, (hours-four)/four)) + # Probably rounded based on https://kraken-fees-calculator.github.io/ + return borrowed * rate * (1+ceil(hours/four)) else: raise OperationalException("Leverage not available on this exchange with freqtrade") diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a2ca7badb..2428c7d24 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -609,14 +609,12 @@ class LocalTrade(): def update_order(self, order: Dict) -> None: Order.update_orders(self.orders, order) - def _calc_open_trade_value(self, amount: Optional[float] = None) -> float: + def _calc_open_trade_value(self) -> float: """ Calculate the open_rate including open_fee. :return: Price in of the open trade incl. Fees """ - if amount is None: - amount = self.amount - open_trade = Decimal(amount) * Decimal(self.open_rate) + open_trade = Decimal(self.amount) * Decimal(self.open_rate) fees = open_trade * Decimal(self.fee_open) if self.is_short: return float(open_trade - fees) @@ -653,7 +651,6 @@ class LocalTrade(): return self.interest_mode(borrowed=borrowed, rate=rate, hours=hours) def calc_close_trade_value(self, rate: Optional[float] = None, - fee: Optional[float] = None, interest_rate: Optional[float] = None) -> float: """ @@ -721,30 +718,23 @@ class LocalTrade(): If interest_rate is not set self.interest_rate will be used :return: profit ratio as float """ - close_trade_value = self.calc_close_trade_value( rate=(rate or self.close_rate), fee=(fee or self.fee_close), interest_rate=(interest_rate or self.interest_rate) ) - if self.leverage is None: - leverage = 1.0 - else: - leverage = self.leverage - - stake_value = self._calc_open_trade_value(amount=(self.amount/leverage)) - 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) + leverage = self.leverage or 1.0 if (short_close_zero or long_close_zero): return 0.0 else: if self.is_short: - profit_ratio = ((self.open_trade_value - close_trade_value) / stake_value) + profit_ratio = (1 - (close_trade_value/self.open_trade_value)) * leverage else: - profit_ratio = ((close_trade_value - self.open_trade_value) / stake_value) + profit_ratio = ((close_trade_value/self.open_trade_value) - 1) * leverage return float(f"{profit_ratio:.8f}") diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index e4290231c..00ffd3fe4 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -434,22 +434,22 @@ def leverage_trade(fee): stake_amount: 15.129 base borrowed: 60.516 base leverage: 5 - time-periods: 5 hrs( 5/4 time-period of 4 hours) - interest: borrowed * interest_rate * time-periods - = 60.516 * 0.0005 * 5/4 = 0.0378225 base + hours: 5 + interest: borrowed * interest_rate * ceil(1 + hours/4) + = 60.516 * 0.0005 * ceil(1 + 5/4) = 0.090774 base open_value: (amount * open_rate) + (amount * open_rate * fee) = (615.0 * 0.123) + (615.0 * 0.123 * 0.0025) = 75.83411249999999 close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest - = (615.0 * 0.128) - (615.0 * 0.128 * 0.0025) - 0.0378225 - = 78.4853775 + = (615.0 * 0.128) - (615.0 * 0.128 * 0.0025) - 0.090774 + = 78.432426 total_profit = close_value - open_value - = 78.4853775 - 75.83411249999999 - = 2.6512650000000093 - total_profit_percentage = total_profit / stake_amount - = 2.6512650000000093 / 15.129 - = 0.17524390243902502 + = 78.432426 - 75.83411249999999 + = 2.5983135000000175 + total_profit_percentage = ((close_value/open_value)-1) * leverage + = ((78.432426/75.83411249999999)-1) * 5 + = 0.1713156134055116 """ trade = Trade( pair='DOGE/BTC', @@ -461,8 +461,8 @@ def leverage_trade(fee): fee_close=fee.return_value, open_rate=0.123, close_rate=0.128, - close_profit=0.17524390243902502, - close_profit_abs=2.6512650000000093, + close_profit=0.1713156134055116, + close_profit_abs=2.5983135000000175, exchange='kraken', is_open=False, open_order_id='dry_run_leverage_sell_12345', diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 9adb80b2a..4fc979568 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -238,7 +238,6 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): @pytest.mark.usefixtures("init_persistence") def test_trade_close(limit_buy_order, limit_sell_order, fee): - # TODO: limit_buy_order and limit_sell_order aren't used, remove them probably trade = Trade( pair='ETH/BTC', stake_amount=0.001, diff --git a/tests/persistence/test_persistence_leverage.py b/tests/persistence/test_persistence_leverage.py index d2345163d..a5b5178d1 100644 --- a/tests/persistence/test_persistence_leverage.py +++ b/tests/persistence/test_persistence_leverage.py @@ -27,11 +27,11 @@ def test_interest_kraken_lev(market_lev_buy_order, fee): time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) 5 hours = 5/4 - interest: borrowed * interest_rate * time-periods - = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 base - = 0.0075414886436454 * 0.00025 * 5/4 = 2.3567152011391876e-06 base - = 0.0150829772872908 * 0.0005 * 5/4 = 9.42686080455675e-06 base - = 0.0150829772872908 * 0.00025 * 1 = 3.7707443218227e-06 base + interest: borrowed * interest_rate * ceil(1 + time-periods) + = 0.0075414886436454 * 0.0005 * ceil(2) = 7.5414886436454e-06 base + = 0.0075414886436454 * 0.00025 * ceil(9/4) = 5.65611648273405e-06 base + = 0.0150829772872908 * 0.0005 * ceil(9/4) = 2.26244659309362e-05 base + = 0.0150829772872908 * 0.00025 * ceil(2) = 7.5414886436454e-06 base """ trade = Trade( @@ -48,19 +48,17 @@ def test_interest_kraken_lev(market_lev_buy_order, fee): interest_mode=InterestMode.HOURSPER4 ) - # 10 minutes round up to 4 hours evenly on kraken so we can predict the exact value - assert float(trade.calculate_interest()) == 3.7707443218227e-06 - trade.open_date = datetime.utcnow() - timedelta(hours=5, minutes=0) - # All trade > 5 hours will vary slightly due to execution time and interest calculated + assert float(trade.calculate_interest()) == 7.5414886436454e-06 + trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55) assert float(round(trade.calculate_interest(interest_rate=0.00025), 11) - ) == round(2.3567152011391876e-06, 11) + ) == round(5.65611648273405e-06, 11) trade = Trade( pair='ETH/BTC', stake_amount=0.0037707443218227, amount=459.95905365, open_rate=0.00001099, - open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), + open_date=datetime.utcnow() - timedelta(hours=4, minutes=55), fee_open=fee.return_value, fee_close=fee.return_value, exchange='kraken', @@ -70,9 +68,10 @@ def test_interest_kraken_lev(market_lev_buy_order, fee): ) assert float(round(trade.calculate_interest(), 11) - ) == round(9.42686080455675e-06, 11) + ) == round(2.26244659309362e-05, 11) trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10) - assert float(trade.calculate_interest(interest_rate=0.00025)) == 3.7707443218227e-06 + trade.interest_rate = 0.00025 + assert float(trade.calculate_interest(interest_rate=0.00025)) == 7.5414886436454e-06 @pytest.mark.usefixtures("init_persistence") @@ -116,7 +115,7 @@ def test_interest_binance_lev(market_lev_buy_order, fee): ) # 10 minutes round up to 4 hours evenly on kraken so we can predict the them more accurately assert round(float(trade.calculate_interest()), 22) == round(4.166666666344583e-08, 22) - trade.open_date = datetime.utcnow() - timedelta(hours=5, minutes=0) + trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55) # All trade > 5 hours will vary slightly due to execution time and interest calculated assert float(round(trade.calculate_interest(interest_rate=0.00025), 14) ) == round(1.0416666665861459e-07, 14) @@ -126,7 +125,7 @@ def test_interest_binance_lev(market_lev_buy_order, fee): stake_amount=0.0009999999999226999, amount=459.95905365, open_rate=0.00001099, - open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), + open_date=datetime.utcnow() - timedelta(hours=4, minutes=55), fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', @@ -178,7 +177,7 @@ def test_calc_open_trade_value_lev(market_lev_buy_order, fee): borrowed: 0.0075414886436454 base time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) interest: borrowed * interest_rate * time-periods - = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto + = 0.0075414886436454 * 0.0005 * 1 = 7.5414886436454e-06 crypto open_value: (amount * open_rate) + (amount * open_rate * fee) = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) = 0.01134051354788177 @@ -243,7 +242,7 @@ def test_calc_open_close_trade_price_lev(limit_lev_buy_order, limit_lev_sell_ord stake_amount=0.0009999999999226999, open_rate=0.01, amount=5, - open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), + open_date=datetime.utcnow() - timedelta(hours=4, minutes=55), fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', @@ -275,23 +274,20 @@ def test_trade_close_lev(fee): stake_amount: 0.5 borrowed: 1 base time-periods: 5/4 periods of 4hrs - interest: borrowed * interest_rate * time-periods - = 1 * 0.0005 * 5/4 = 0.000625 crypto + interest: borrowed * interest_rate * ceil(1 + time-periods) + = 1 * 0.0005 * ceil(9/4) = 0.0015 crypto open_value: (amount * open_rate) + (amount * open_rate * fee) = (15 * 0.1) + (15 * 0.1 * 0.0025) = 1.50375 close_value: (amount * close_rate) + (amount * close_rate * fee) - interest - = (15 * 0.2) - (15 * 0.2 * 0.0025) - 0.000625 - = 2.9918750000000003 - stake_value: (amount/lev * open_rate) + (amount/lev * open_rate * fee) - = ((15/3) * 0.1) + ((15/3) * 0.1 * 0.0025) - = 0.50125 + = (15 * 0.2) - (15 * 0.2 * 0.0025) - 0.0015 + = 2.991 total_profit = close_value - open_value - = 2.9918750000000003 - 1.50375 - = 1.4881250000000001 - total_profit_ratio = total_profit / stake_value - = 1.4881250000000001 / 0.50125 - = 2.968827930174564 + = 2.991 - 1.50375 + = 1.4872500000000002 + total_profit_ratio = ((close_value/open_value) - 1) * leverage + = ((2.991/1.50375) - 1) * 3 + = 2.96708229426434 """ trade = Trade( pair='ETH/BTC', @@ -301,7 +297,7 @@ def test_trade_close_lev(fee): is_open=True, fee_open=fee.return_value, fee_close=fee.return_value, - open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), + open_date=datetime.utcnow() - timedelta(hours=4, minutes=55), exchange='kraken', leverage=3.0, interest_rate=0.0005, @@ -312,7 +308,7 @@ def test_trade_close_lev(fee): assert trade.is_open is True trade.close(0.2) assert trade.is_open is False - assert trade.close_profit == round(2.968827930174564, 8) + assert trade.close_profit == round(2.96708229426434, 8) assert trade.close_date is not None # TODO-mg: Remove these comments probably @@ -337,19 +333,19 @@ def test_calc_close_trade_price_lev(market_lev_buy_order, market_lev_sell_order, amount: 91.99181073 * leverage(3) = 275.97543219 crypto stake_amount: 0.0037707443218227 borrowed: 0.0075414886436454 base - time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) + time-periods: 10 minutes = 2 interest: borrowed * interest_rate * time-periods - = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto + = 0.0075414886436454 * 0.0005 * 2 = 7.5414886436454e-06 crypto open_value: (amount * open_rate) + (amount * open_rate * fee) = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) = 0.01134051354788177 close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest - = (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.0025) - 3.7707443218227e-06 - = 0.003393252246819716 - = (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.003) - 3.7707443218227e-06 - = 0.003391549478403104 - = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.005) - 3.7707443218227e-06 - = 0.011455101767040435 + = (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.0025) - 7.5414886436454e-06 + = 0.0033894815024978933 + = (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.003) - 7.5414886436454e-06 + = 0.003387778734081281 + = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.005) - 7.5414886436454e-06 + = 0.011451331022718612 """ trade = Trade( pair='ETH/BTC', @@ -367,12 +363,12 @@ def test_calc_close_trade_price_lev(market_lev_buy_order, market_lev_sell_order, trade.open_order_id = 'close_trade' trade.update(market_lev_buy_order) # Buy @ 0.00001099 # Get the close rate price with a custom close rate and a regular fee rate - assert isclose(trade.calc_close_trade_value(rate=0.00001234), 0.003393252246819716) + assert isclose(trade.calc_close_trade_value(rate=0.00001234), 0.0033894815024978933) # Get the close rate price with a custom close rate and a custom fee rate - assert isclose(trade.calc_close_trade_value(rate=0.00001234, fee=0.003), 0.003391549478403104) + assert isclose(trade.calc_close_trade_value(rate=0.00001234, fee=0.003), 0.003387778734081281) # Test when we apply a Sell order, and ask price with a custom fee rate trade.update(market_lev_sell_order) - assert isclose(trade.calc_close_trade_value(fee=0.005), 0.011455101767040435) + assert isclose(trade.calc_close_trade_value(fee=0.005), 0.011451331022718612) @pytest.mark.usefixtures("init_persistence") @@ -394,6 +390,9 @@ def test_update_limit_order_lev(limit_lev_buy_order, limit_lev_sell_order, fee, open_value: (amount * open_rate) + (amount * open_rate * fee) = (272.97543219 * 0.00001099) + (272.97543219 * 0.00001099 * 0.0025) = 0.0030074999997675204 + stake_value = (amount/lev * open_rate) + (amount/lev * open_rate * fee) + = (272.97543219 * 0.00001099) + (272.97543219 * 0.00001099 * 0.0025) + = 0.0010024999999225066 close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) = (272.97543219 * 0.00001173) - (272.97543219 * 0.00001173 * 0.0025) = 0.003193996815039728 @@ -460,24 +459,23 @@ def test_update_market_order_lev(market_lev_buy_order, market_lev_sell_order, fe amount: = 275.97543219 crypto stake_amount: 0.0037707443218227 borrowed: 0.0075414886436454 base - time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) - interest: borrowed * interest_rate * time-periods - = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto + interest: borrowed * interest_rate * 1+ceil(hours) + = 0.0075414886436454 * 0.0005 * (1+ceil(1)) = 7.5414886436454e-06 crypto open_value: (amount * open_rate) + (amount * open_rate * fee) = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) = 0.01134051354788177 - close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.0025) - = 0.011487663648325479 + close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest + = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.0025) - 7.5414886436454e-06 + = 0.011480122159681833 stake_value: (amount/lev * open_rate) + (amount/lev * open_rate * fee) = (275.97543219/3 * 0.00004099) + (275.97543219/3 * 0.00004099 * 0.0025) = 0.0037801711826272568 - total_profit = close_value - open_value - interest - = 0.011487663648325479 - 0.01134051354788177 - 3.7707443218227e-06 - = 0.0001433793561218866 - total_profit_percentage = total_profit / stake_value - = 0.0001433793561218866 / 0.0037801711826272568 - = 0.03792932890997717 + total_profit = close_value - open_value + = 0.011480122159681833 - 0.01134051354788177 + = 0.00013960861180006392 + total_profit_percentage = ((close_value/open_value) - 1) * leverage + = ((0.011480122159681833 / 0.01134051354788177)-1) * 3 + = 0.036931822675563275 """ trade = Trade( id=1, @@ -512,7 +510,7 @@ def test_update_market_order_lev(market_lev_buy_order, market_lev_sell_order, fe trade.update(market_lev_sell_order) assert trade.open_order_id is None assert trade.close_rate == 0.00004173 - assert trade.close_profit == round(0.03792932890997717, 8) + assert trade.close_profit == round(0.036931822675563275, 8) assert trade.close_date is not None # TODO: The amount should maybe be the opening amount + the interest # TODO: Uncomment the next assert and make it work. @@ -552,39 +550,38 @@ def test_calc_profit_lev(market_lev_buy_order, market_lev_sell_order, fee): stake_amount: 0.0037707443218227 amount: 91.99181073 * leverage(3) = 275.97543219 crypto borrowed: 0.0075414886436454 base - time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) - 5 hours = 5/4 + hours: 1/6, 5 hours - interest: borrowed * interest_rate * time-periods - = 0.0075414886436454 * 0.0005 * 1 = 3.7707443218227e-06 crypto - = 0.0075414886436454 * 0.00025 * 5/4 = 2.3567152011391876e-06 crypto - = 0.0075414886436454 * 0.0005 * 5/4 = 4.713430402278375e-06 crypto - = 0.0075414886436454 * 0.00025 * 1 = 1.88537216091135e-06 crypto + interest: borrowed * interest_rate * ceil(1+hours/4) + = 0.0075414886436454 * 0.0005 * ceil(1+((1/6)/4)) = 7.5414886436454e-06 crypto + = 0.0075414886436454 * 0.00025 * ceil(1+(5/4)) = 5.65611648273405e-06 crypto + = 0.0075414886436454 * 0.0005 * ceil(1+(5/4)) = 1.13122329654681e-05 crypto + = 0.0075414886436454 * 0.00025 * ceil(1+((1/6)/4)) = 3.7707443218227e-06 crypto open_value: (amount * open_rate) + (amount * open_rate * fee) = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) = 0.01134051354788177 close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest - (275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.0025) - 3.7707443218227e-06 - = 0.01479007168225405 - (275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.0025) - 2.3567152011391876e-06 - = 0.001200640891872485 - (275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.003) - 4.713430402278375e-06 - = 0.014781713536310649 - (275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.003) - 1.88537216091135e-06 - = 0.0012005092285933775 + (275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.0025) - 7.5414886436454e-06 + = 0.014786300937932227 + (275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.0025) - 5.65611648273405e-06 + = 0.0011973414905908902 + (275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.003) - 1.13122329654681e-05 + = 0.01477511473374746 + (275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.003) - 3.7707443218227e-06 + = 0.0011986238564324662 stake_value: (amount/lev * open_rate) + (amount/lev * open_rate * fee) = (275.97543219/3 * 0.00004099) + (275.97543219/3 * 0.00004099 * 0.0025) = 0.0037801711826272568 total_profit = close_value - open_value - = 0.01479007168225405 - 0.01134051354788177 = 0.003449558134372281 - = 0.001200640891872485 - 0.01134051354788177 = -0.010139872656009285 - = 0.014781713536310649 - 0.01134051354788177 = 0.0034411999884288794 - = 0.0012005092285933775 - 0.01134051354788177 = -0.010140004319288392 - total_profit_percentage = total_profit / stake_value - 0.003449558134372281/0.0037801711826272568 = 0.9125401913610705 - -0.010139872656009285/0.0037801711826272568 = -2.682384518089991 - 0.0034411999884288794/0.0037801711826272568 = 0.9103291417710906 - -0.010140004319288392/0.0037801711826272568 = -2.6824193480679854 + = 0.014786300937932227 - 0.01134051354788177 = 0.0034457873900504577 + = 0.0011973414905908902 - 0.01134051354788177 = -0.01014317205729088 + = 0.01477511473374746 - 0.01134051354788177 = 0.00343460118586569 + = 0.0011986238564324662 - 0.01134051354788177 = -0.010141889691449303 + total_profit_percentage = ((close_value/open_value) - 1) * leverage + ((0.014786300937932227/0.01134051354788177) - 1) * 3 = 0.9115426851266561 + ((0.0011973414905908902/0.01134051354788177) - 1) * 3 = -2.683257336045103 + ((0.01477511473374746/0.01134051354788177) - 1) * 3 = 0.908583505860866 + ((0.0011986238564324662/0.01134051354788177) - 1) * 3 = -2.6829181011851926 """ trade = Trade( @@ -606,35 +603,35 @@ def test_calc_profit_lev(market_lev_buy_order, market_lev_sell_order, fee): # Higher than open rate assert trade.calc_profit(rate=0.00005374, interest_rate=0.0005) == round( - 0.003449558134372281, 8) + 0.0034457873900504577, 8) assert trade.calc_profit_ratio( - rate=0.00005374, interest_rate=0.0005) == round(0.9125401913610705, 8) + rate=0.00005374, interest_rate=0.0005) == round(0.9115426851266561, 8) # Lower than open rate - trade.open_date = datetime.utcnow() - timedelta(hours=5, minutes=0) + trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55) assert trade.calc_profit( - rate=0.00000437, interest_rate=0.00025) == round(-0.010139872656009285, 8) + rate=0.00000437, interest_rate=0.00025) == round(-0.01014317205729088, 8) assert trade.calc_profit_ratio( - rate=0.00000437, interest_rate=0.00025) == round(-2.682384518089991, 8) + rate=0.00000437, interest_rate=0.00025) == round(-2.683257336045103, 8) # Custom closing rate and custom fee rate # Higher than open rate assert trade.calc_profit(rate=0.00005374, fee=0.003, - interest_rate=0.0005) == round(0.0034411999884288794, 8) + interest_rate=0.0005) == round(0.00343460118586569, 8) assert trade.calc_profit_ratio(rate=0.00005374, fee=0.003, - interest_rate=0.0005) == round(0.9103291417710906, 8) + interest_rate=0.0005) == round(0.908583505860866, 8) # Lower than open rate trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10) assert trade.calc_profit(rate=0.00000437, fee=0.003, - interest_rate=0.00025) == round(-0.010140004319288392, 8) + interest_rate=0.00025) == round(-0.010141889691449303, 8) assert trade.calc_profit_ratio(rate=0.00000437, fee=0.003, - interest_rate=0.00025) == round(-2.6824193480679854, 8) + interest_rate=0.00025) == round(-2.6829181011851926, 8) # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 trade.update(market_lev_sell_order) - assert trade.calc_profit() == round(0.0001433793561218866, 8) - assert trade.calc_profit_ratio() == round(0.03792932890997717, 8) + assert trade.calc_profit() == round(0.00013960861180006392, 8) + assert trade.calc_profit_ratio() == round(0.036931822675563275, 8) # Test with a custom fee rate on the close trade # assert trade.calc_profit(fee=0.003) == 0.00006163 diff --git a/tests/persistence/test_persistence_short.py b/tests/persistence/test_persistence_short.py index ba08e1632..2a1e46615 100644 --- a/tests/persistence/test_persistence_short.py +++ b/tests/persistence/test_persistence_short.py @@ -26,11 +26,11 @@ def test_interest_kraken_short(market_short_order, fee): time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) 5 hours = 5/4 - interest: borrowed * interest_rate * time-periods - = 275.97543219 * 0.0005 * 1 = 0.137987716095 crypto - = 275.97543219 * 0.00025 * 5/4 = 0.086242322559375 crypto - = 459.95905365 * 0.0005 * 5/4 = 0.28747440853125 crypto - = 459.95905365 * 0.00025 * 1 = 0.1149897634125 crypto + interest: borrowed * interest_rate * ceil(1 + time-periods) + = 275.97543219 * 0.0005 * ceil(1+1) = 0.27597543219 crypto + = 275.97543219 * 0.00025 * ceil(9/4) = 0.20698157414249999 crypto + = 459.95905365 * 0.0005 * ceil(9/4) = 0.689938580475 crypto + = 459.95905365 * 0.00025 * ceil(1+1) = 0.229979526825 crypto """ trade = Trade( @@ -48,17 +48,17 @@ def test_interest_kraken_short(market_short_order, fee): interest_mode=InterestMode.HOURSPER4 ) - assert float(round(trade.calculate_interest(), 8)) == round(0.137987716095, 8) - trade.open_date = datetime.utcnow() - timedelta(hours=5, minutes=0) + assert float(round(trade.calculate_interest(), 8)) == round(0.27597543219, 8) + trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55) assert float(round(trade.calculate_interest(interest_rate=0.00025), 8) - ) == round(0.086242322559375, 8) + ) == round(0.20698157414249999, 8) trade = Trade( pair='ETH/BTC', stake_amount=0.001, amount=459.95905365, open_rate=0.00001099, - open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), + open_date=datetime.utcnow() - timedelta(hours=4, minutes=55), fee_open=fee.return_value, fee_close=fee.return_value, exchange='kraken', @@ -68,10 +68,10 @@ def test_interest_kraken_short(market_short_order, fee): interest_mode=InterestMode.HOURSPER4 ) - assert float(round(trade.calculate_interest(), 8)) == round(0.28747440853125, 8) + assert float(round(trade.calculate_interest(), 8)) == round(0.689938580475, 8) trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10) assert float(round(trade.calculate_interest(interest_rate=0.00025), 8) - ) == round(0.1149897634125, 8) + ) == round(0.229979526825, 8) @ pytest.mark.usefixtures("init_persistence") @@ -114,7 +114,7 @@ def test_interest_binance_short(market_short_order, fee): ) assert float(round(trade.calculate_interest(), 8)) == 0.00574949 - trade.open_date = datetime.utcnow() - timedelta(hours=5, minutes=0) + trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55) assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)) == 0.01437372 trade = Trade( @@ -122,7 +122,7 @@ def test_interest_binance_short(market_short_order, fee): stake_amount=0.001, amount=459.95905365, open_rate=0.00001099, - open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), + open_date=datetime.utcnow() - timedelta(hours=4, minutes=55), fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', @@ -218,13 +218,13 @@ def test_calc_close_trade_price_short(market_short_order, market_exit_short_orde close_rate: 0.00001234 base amount: = 275.97543219 crypto borrowed: 275.97543219 crypto - time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) - interest: borrowed * interest_rate * time-periods - = 275.97543219 * 0.0005 * 1 = 0.137987716095 crypto - amount_closed: amount + interest = 275.97543219 + 0.137987716095 = 276.113419906095 + hours: 10 minutes = 1/6 + interest: borrowed * interest_rate * ceil(1 + hours/4) + = 275.97543219 * 0.0005 * ceil(1 + ((1/6)/4)) = 0.27597543219 crypto + amount_closed: amount + interest = 275.97543219 + 0.27597543219 = 276.25140762219 close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - = (276.113419906095 * 0.00001234) + (276.113419906095 * 0.00001234 * 0.0025) - = 0.01134618380465571 + = (276.25140762219 * 0.00004099) + (276.25140762219 * 0.00004099 * 0.005) + = 0.011380162924425737 """ trade = Trade( pair='ETH/BTC', @@ -243,12 +243,12 @@ def test_calc_close_trade_price_short(market_short_order, market_exit_short_orde trade.open_order_id = 'close_trade' trade.update(market_short_order) # Buy @ 0.00001099 # Get the close rate price with a custom close rate and a regular fee rate - assert isclose(trade.calc_close_trade_value(rate=0.00001234), 0.003415757700645315) + assert isclose(trade.calc_close_trade_value(rate=0.00001234), 0.0034174647259) # Get the close rate price with a custom close rate and a custom fee rate - assert isclose(trade.calc_close_trade_value(rate=0.00001234, fee=0.003), 0.0034174613204461354) + assert isclose(trade.calc_close_trade_value(rate=0.00001234, fee=0.003), 0.0034191691971679986) # Test when we apply a Sell order, and ask price with a custom fee rate trade.update(market_exit_short_order) - assert isclose(trade.calc_close_trade_value(fee=0.005), 0.011374478527360586) + assert isclose(trade.calc_close_trade_value(fee=0.005), 0.011380162924425737) @ pytest.mark.usefixtures("init_persistence") @@ -273,19 +273,21 @@ def test_calc_open_close_trade_price_short(limit_short_order, limit_exit_short_o close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) = (91.0012890436177 * 0.00001099) + (91.0012890436177 * 0.00001099 * 0.0025) = 0.001002604427005832 + stake_value = (amount/lev * open_rate) - (amount/lev * open_rate * fee) + = 0.0010646656050132426 total_profit = open_value - close_value = 0.0010646656050132426 - 0.001002604427005832 = 0.00006206117800741065 - total_profit_percentage = (close_value - open_value) / stake_amount - = (0.0010646656050132426 - 0.0010025208853391716)/0.0010673339398629 - = 0.05822425142973869 + total_profit_percentage = (close_value - open_value) / stake_value + = (0.0010646656050132426 - 0.001002604427005832)/0.0010646656050132426 + = 0.05829170935473088 """ trade = Trade( pair='ETH/BTC', stake_amount=0.0010673339398629, open_rate=0.01, amount=5, - open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), + open_date=datetime.utcnow() - timedelta(hours=4, minutes=55), fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', @@ -302,8 +304,7 @@ def test_calc_open_close_trade_price_short(limit_short_order, limit_exit_short_o # Profit in BTC assert round(trade.calc_profit(), 8) == round(0.00006206117800741065, 8) # Profit in percent - # TODO-mg get this working - # assert round(trade.calc_profit_ratio(), 11) == round(0.05822425142973869, 11) + assert round(trade.calc_profit_ratio(), 8) == round(0.05829170935473088, 8) @ pytest.mark.usefixtures("init_persistence") @@ -322,20 +323,20 @@ def test_trade_close_short(fee): time-periods: 5 hours = 5/4 interest: borrowed * interest_rate * time-periods - = 15 * 0.0005 * 5/4 = 0.009375 crypto + = 15 * 0.0005 * ceil(1 + 5/4) = 0.0225 crypto open_value: (amount * open_rate) - (amount * open_rate * fee) = (15 * 0.02) - (15 * 0.02 * 0.0025) = 0.29925 - amount_closed: amount + interest = 15 + 0.009375 = 15.009375 + amount_closed: amount + interest = 15 + 0.009375 = 15.0225 close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - = (15.009375 * 0.01) + (15.009375 * 0.01 * 0.0025) - = 0.150468984375 + = (15.0225 * 0.01) + (15.0225 * 0.01 * 0.0025) + = 0.15060056250000003 total_profit = open_value - close_value - = 0.29925 - 0.150468984375 - = 0.148781015625 - total_profit_percentage = total_profit / stake_amount - = 0.148781015625 / 0.1 - = 1.4878101562500001 + = 0.29925 - 0.15060056250000003 + = 0.14864943749999998 + total_profit_percentage = (1-(close_value/open_value)) * leverage + = (1 - (0.15060056250000003/0.29925)) * 3 + = 1.4902199248120298 """ trade = Trade( pair='ETH/BTC', @@ -345,7 +346,7 @@ def test_trade_close_short(fee): is_open=True, fee_open=fee.return_value, fee_close=fee.return_value, - open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), + open_date=datetime.utcnow() - timedelta(hours=4, minutes=55), exchange='kraken', is_short=True, leverage=3.0, @@ -357,7 +358,7 @@ def test_trade_close_short(fee): assert trade.is_open is True trade.close(0.01) assert trade.is_open is False - assert trade.close_profit == round(1.4878101562500001, 8) + assert trade.close_profit == round(1.4902199248120298, 8) assert trade.close_date is not None # TODO-mg: Remove these comments probably @@ -396,9 +397,9 @@ def test_update_with_binance_short(limit_short_order, limit_exit_short_order, fe total_profit = open_value - close_value = 0.0010646656050132426 - 0.0010025208853391716 = 0.00006214471967407108 - total_profit_percentage = (close_value - open_value) / stake_amount - = 0.00006214471967407108 / 0.0010673339398629 - = 0.05822425142973869 + total_profit_percentage = (1 - (close_value/open_value)) * leverage + = (1 - (0.0010025208853391716/0.0010646656050132426)) * 1 + = 0.05837017687191848 """ trade = Trade( @@ -437,7 +438,7 @@ def test_update_with_binance_short(limit_short_order, limit_exit_short_order, fe trade.update(limit_exit_short_order) # assert trade.open_order_id is None assert trade.close_rate == 0.00001099 - assert trade.close_profit == 0.05822425 + assert trade.close_profit == round(0.05837017687191848, 8) assert trade.close_date is not None assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, " r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001173, open_since=.*\).", @@ -463,20 +464,21 @@ def test_update_market_order_short( borrowed: 275.97543219 crypto time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) interest: borrowed * interest_rate * time-periods - = 275.97543219 * 0.0005 * 1 = 0.137987716095 crypto + = 275.97543219 * 0.0005 * 2 = 0.27597543219 crypto open_value: (amount * open_rate) - (amount * open_rate * fee) = 275.97543219 * 0.00004173 - 275.97543219 * 0.00004173 * 0.0025 = 0.011487663648325479 - amount_closed: amount + interest = 275.97543219 + 0.137987716095 = 276.113419906095 + amount_closed: amount + interest = 275.97543219 + 0.27597543219 = 276.25140762219 close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - = (276.113419906095 * 0.00004099) + (276.113419906095 * 0.00004099 * 0.0025) - = 0.01134618380465571 + = (276.25140762219 * 0.00004099) + (276.25140762219 * 0.00004099 * 0.0025) + = 0.0034174647259 total_profit = open_value - close_value - = 0.011487663648325479 - 0.01134618380465571 - = 0.00014147984366976937 + = 0.011487663648325479 - 0.0034174647259 + = 0.00013580958689582596 total_profit_percentage = total_profit / stake_amount - = 0.00014147984366976937 / 0.0038388182617629 - = 0.036855051222142936 + = (1 - (close_value/open_value)) * leverage + = (1 - (0.0034174647259/0.011487663648325479)) * 3 + = 0.03546663387440563 """ trade = Trade( id=1, @@ -511,7 +513,7 @@ def test_update_market_order_short( trade.update(market_exit_short_order) assert trade.open_order_id is None assert trade.close_rate == 0.00004099 - assert trade.close_profit == 0.03685505 + assert trade.close_profit == round(0.03546663387440563, 8) assert trade.close_date is not None # TODO-mg: The amount should maybe be the opening amount + the interest # TODO-mg: Uncomment the next assert and make it work. @@ -527,7 +529,7 @@ def test_calc_profit_short(market_short_order, market_exit_short_order, fee): Market trade on Kraken at 3x leverage Short trade fee: 0.25% base or 0.3% - interest_rate: 0.05%, 0.25% per 4 hrs + interest_rate: 0.05%, 0.025% per 4 hrs open_rate: 0.00004173 base close_rate: 0.00004099 base stake_amount: 0.0038388182617629 @@ -537,38 +539,42 @@ def test_calc_profit_short(market_short_order, market_exit_short_order, fee): 5 hours = 5/4 interest: borrowed * interest_rate * time-periods - = 275.97543219 * 0.0005 * 1 = 0.137987716095 crypto - = 275.97543219 * 0.00025 * 5/4 = 0.086242322559375 crypto - = 275.97543219 * 0.0005 * 5/4 = 0.17248464511875 crypto - = 275.97543219 * 0.00025 * 1 = 0.0689938580475 crypto + = 275.97543219 * 0.0005 * ceil(1+1) = 0.27597543219 crypto + = 275.97543219 * 0.00025 * ceil(1+5/4) = 0.20698157414249999 crypto + = 275.97543219 * 0.0005 * ceil(1+5/4) = 0.41396314828499997 crypto + = 275.97543219 * 0.00025 * ceil(1+1) = 0.27597543219 crypto + = 275.97543219 * 0.00025 * ceil(1+1) = 0.27597543219 crypto open_value: (amount * open_rate) - (amount * open_rate * fee) = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.0025) = 0.011487663648325479 amount_closed: amount + interest - = 275.97543219 + 0.137987716095 = 276.113419906095 - = 275.97543219 + 0.086242322559375 = 276.06167451255936 - = 275.97543219 + 0.17248464511875 = 276.14791683511874 - = 275.97543219 + 0.0689938580475 = 276.0444260480475 + = 275.97543219 + 0.27597543219 = 276.25140762219 + = 275.97543219 + 0.20698157414249999 = 276.1824137641425 + = 275.97543219 + 0.41396314828499997 = 276.389395338285 + = 275.97543219 + 0.27597543219 = 276.25140762219 close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - (276.113419906095 * 0.00004374) + (276.113419906095 * 0.00004374 * 0.0025) - = 0.012107393989159325 - (276.06167451255936 * 0.00000437) + (276.06167451255936 * 0.00000437 * 0.0025) - = 0.0012094054914139338 - (276.14791683511874 * 0.00004374) + (276.14791683511874 * 0.00004374 * 0.003) - = 0.012114946012015198 - (276.0444260480475 * 0.00000437) + (276.0444260480475 * 0.00000437 * 0.003) - = 0.0012099330842554573 + (276.25140762219 * 0.00004374) + (276.25140762219 * 0.00004374 * 0.0025) + = 0.012113444660818078 + (276.1824137641425 * 0.00000437) + (276.1824137641425 * 0.00000437 * 0.0025) + = 0.0012099344410196758 + (276.389395338285 * 0.00004374) + (276.389395338285 * 0.00004374 * 0.003) + = 0.012125539968552874 + (276.25140762219 * 0.00000437) + (276.25140762219 * 0.00000437 * 0.003) + = 0.0012102354919246037 + (276.25140762219 * 0.00004099) + (276.25140762219 * 0.00004099 * 0.0025) + = 0.011351854061429653 total_profit = open_value - close_value - = print(0.011487663648325479 - 0.012107393989159325) = -0.0006197303408338461 - = print(0.011487663648325479 - 0.0012094054914139338) = 0.010278258156911545 - = print(0.011487663648325479 - 0.012114946012015198) = -0.0006272823636897188 - = print(0.011487663648325479 - 0.0012099330842554573) = 0.010277730564070022 - total_profit_percentage = (close_value - open_value) / stake_amount - (0.011487663648325479 - 0.012107393989159325)/0.0038388182617629 = -0.16143779115744006 - (0.011487663648325479 - 0.0012094054914139338)/0.0038388182617629 = 2.677453699564163 - (0.011487663648325479 - 0.012114946012015198)/0.0038388182617629 = -0.16340506919482353 - (0.011487663648325479 - 0.0012099330842554573)/0.0038388182617629 = 2.677316263299785 - + = 0.011487663648325479 - 0.012113444660818078 = -0.0006257810124925996 + = 0.011487663648325479 - 0.0012099344410196758 = 0.010277729207305804 + = 0.011487663648325479 - 0.012125539968552874 = -0.0006378763202273957 + = 0.011487663648325479 - 0.0012102354919246037 = 0.010277428156400875 + = 0.011487663648325479 - 0.011351854061429653 = 0.00013580958689582596 + total_profit_percentage = (1-(close_value/open_value)) * leverage + (1-(0.012113444660818078 /0.011487663648325479))*3 = -0.16342252828332549 + (1-(0.0012099344410196758/0.011487663648325479))*3 = 2.6840259748040123 + (1-(0.012125539968552874 /0.011487663648325479))*3 = -0.16658121435868578 + (1-(0.0012102354919246037/0.011487663648325479))*3 = 2.68394735544829 + (1-(0.011351854061429653/0.011487663648325479))*3 = 0.03546663387440563 """ trade = Trade( pair='ETH/BTC', @@ -588,34 +594,36 @@ def test_calc_profit_short(market_short_order, market_exit_short_order, fee): # Custom closing rate and regular fee rate # Higher than open rate - assert trade.calc_profit(rate=0.00004374, interest_rate=0.0005) == round(-0.00061973, 8) + assert trade.calc_profit( + rate=0.00004374, interest_rate=0.0005) == round(-0.0006257810124925996, 8) assert trade.calc_profit_ratio( - rate=0.00004374, interest_rate=0.0005) == round(-0.16143779115744006, 8) + rate=0.00004374, interest_rate=0.0005) == round(-0.16342252828332549, 8) # Lower than open rate - trade.open_date = datetime.utcnow() - timedelta(hours=5, minutes=0) - assert trade.calc_profit(rate=0.00000437, interest_rate=0.00025) == round(0.01027826, 8) + trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55) + assert trade.calc_profit(rate=0.00000437, interest_rate=0.00025) == round( + 0.010277729207305804, 8) assert trade.calc_profit_ratio( - rate=0.00000437, interest_rate=0.00025) == round(2.677453699564163, 8) + rate=0.00000437, interest_rate=0.00025) == round(2.6840259748040123, 8) # Custom closing rate and custom fee rate # Higher than open rate assert trade.calc_profit(rate=0.00004374, fee=0.003, - interest_rate=0.0005) == round(-0.00062728, 8) + interest_rate=0.0005) == round(-0.0006378763202273957, 8) assert trade.calc_profit_ratio(rate=0.00004374, fee=0.003, - interest_rate=0.0005) == round(-0.16340506919482353, 8) + interest_rate=0.0005) == round(-0.16658121435868578, 8) # Lower than open rate trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10) assert trade.calc_profit(rate=0.00000437, fee=0.003, - interest_rate=0.00025) == round(0.01027773, 8) + interest_rate=0.00025) == round(0.010277428156400875, 8) assert trade.calc_profit_ratio(rate=0.00000437, fee=0.003, - interest_rate=0.00025) == round(2.677316263299785, 8) + interest_rate=0.00025) == round(2.68394735544829, 8) - # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 + # Test when we apply a exit short order. trade.update(market_exit_short_order) - assert trade.calc_profit() == round(0.00014148, 8) - assert trade.calc_profit_ratio() == round(0.03685505, 8) + assert trade.calc_profit(rate=0.00004099) == round(0.00013580958689582596, 8) + assert trade.calc_profit_ratio() == round(0.03546663387440563, 8) # Test with a custom fee rate on the close trade # assert trade.calc_profit(fee=0.003) == 0.00006163 @@ -769,4 +777,4 @@ def test_get_best_pair_short(fee): res = Trade.get_best_pair() assert len(res) == 2 assert res[0] == 'DOGE/BTC' - assert res[1] == 0.17524390243902502 + assert res[1] == 0.1713156134055116 From 0d06d7e10849a0e8ebd091f7c720aada544b065e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 12 Jul 2021 19:39:35 -0600 Subject: [PATCH 39/46] updated mkdocs and leverage docs Added tests for set_liquidation_price and set_stop_loss updated params in interestmode enum --- docs/leverage.md | 14 +++++ freqtrade/enums/interestmode.py | 6 +- freqtrade/persistence/migrations.py | 9 ++- freqtrade/persistence/models.py | 51 ++++++++-------- mkdocs.yml | 87 ++++++++++++++------------- tests/conftest_trades.py | 1 + tests/persistence/test_persistence.py | 81 ++++++++++++++++++++++++- 7 files changed, 169 insertions(+), 80 deletions(-) diff --git a/docs/leverage.md b/docs/leverage.md index 9a420e573..c4b975a0b 100644 --- a/docs/leverage.md +++ b/docs/leverage.md @@ -1,3 +1,17 @@ +# Leverage + For shorts, the currency which pays the interest fee for the `borrowed` currency is purchased at the same time of the closing trade (This means that the amount purchased in short closing trades is greater than the amount sold in short opening trades). For longs, the currency which pays the interest fee for the `borrowed` will already be owned by the user and does not need to be purchased. The interest is subtracted from the close_value of the trade. + +## Binance margin trading interest formula + + I (interest) = P (borrowed money) * R (daily_interest/24) * ceiling(T) (in hours) + [source](https://www.binance.com/en/support/faq/360030157812) + +## Kraken margin trading interest formula + + Opening fee = P (borrowed money) * R (quat_hourly_interest) + Rollover fee = P (borrowed money) * R (quat_hourly_interest) * ceiling(T/4) (in hours) + I (interest) = Opening fee + Rollover fee + [source](https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading-) diff --git a/freqtrade/enums/interestmode.py b/freqtrade/enums/interestmode.py index 4128fc7a0..89c71a8b4 100644 --- a/freqtrade/enums/interestmode.py +++ b/freqtrade/enums/interestmode.py @@ -17,14 +17,12 @@ class InterestMode(Enum): HOURSPER4 = "HOURSPER4" # Hours per 4 hour segment NONE = "NONE" - def __call__(self, *args, **kwargs): - - borrowed, rate, hours = kwargs["borrowed"], kwargs["rate"], kwargs["hours"] + def __call__(self, borrowed: Decimal, rate: Decimal, hours: Decimal): if self.name == "HOURSPERDAY": return borrowed * rate * ceil(hours)/twenty_four elif self.name == "HOURSPER4": - # Probably rounded based on https://kraken-fees-calculator.github.io/ + # Rounded based on https://kraken-fees-calculator.github.io/ return borrowed * rate * (1+ceil(hours/four)) else: raise OperationalException("Leverage not available on this exchange with freqtrade") diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index c3b07d1b1..c9fa4259b 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -149,17 +149,16 @@ def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, col # let SQLAlchemy create the schema as required decl_base.metadata.create_all(engine) leverage = get_column_def(cols, 'leverage', '1.0') - is_short = get_column_def(cols, 'is_short', 'False') - # TODO-mg: Should liquidation price go in here? + # is_short = get_column_def(cols, 'is_short', 'False') + with engine.begin() as connection: connection.execute(text(f""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, average, remaining, cost, - order_date, order_filled_date, order_update_date, leverage, is_short) + order_date, order_filled_date, order_update_date, leverage) select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, null average, remaining, cost, - order_date, order_filled_date, order_update_date, - {leverage} leverage, {is_short} is_short + order_date, order_filled_date, order_update_date, {leverage} leverage from {table_back_name} """)) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 2428c7d24..69a103123 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -133,7 +133,6 @@ class Order(_DECL_BASE): order_update_date = Column(DateTime, nullable=True) leverage = Column(Float, nullable=True, default=1.0) - is_short = Column(Boolean, nullable=True, default=False) def __repr__(self): @@ -159,7 +158,7 @@ class Order(_DECL_BASE): self.remaining = order.get('remaining', self.remaining) self.cost = order.get('cost', self.cost) self.leverage = order.get('leverage', self.leverage) - # TODO-mg: is_short? + if 'timestamp' in order and order['timestamp'] is not None: self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc) @@ -301,44 +300,42 @@ class LocalTrade(): def __init__(self, **kwargs): for key in kwargs: setattr(self, key, kwargs[key]) - self.set_liquidation_price(self.liquidation_price) + if self.liquidation_price: + self.set_liquidation_price(self.liquidation_price) self.recalc_open_trade_value() - def set_stop_loss_helper(self, stop_loss: Optional[float], liquidation_price: Optional[float]): - """Helper function for set_liquidation_price and set_stop_loss""" - # Stoploss would be better as a computed variable, - # but that messes up the database so it might not be possible - - if liquidation_price is not None: - if stop_loss is not None: - if self.is_short: - self.stop_loss = min(stop_loss, liquidation_price) - else: - self.stop_loss = max(stop_loss, liquidation_price) - else: - self.stop_loss = liquidation_price - self.initial_stop_loss = liquidation_price - self.liquidation_price = liquidation_price - else: - # programmming error check: 1 of liqudication_price or stop_loss must be set - assert stop_loss is not None - if not self.stop_loss: - self.initial_stop_loss = stop_loss - self.stop_loss = stop_loss - def set_stop_loss(self, stop_loss: float): """ Method you should use to set self.stop_loss. Assures stop_loss is not passed the liquidation price """ - self.set_stop_loss_helper(stop_loss=stop_loss, liquidation_price=self.liquidation_price) + if self.liquidation_price is not None: + if self.is_short: + sl = min(stop_loss, self.liquidation_price) + else: + sl = max(stop_loss, self.liquidation_price) + else: + sl = stop_loss + + if not self.stop_loss: + self.initial_stop_loss = sl + self.stop_loss = sl def set_liquidation_price(self, liquidation_price: float): """ Method you should use to set self.liquidation price. Assures stop_loss is not passed the liquidation price """ - self.set_stop_loss_helper(stop_loss=self.stop_loss, liquidation_price=liquidation_price) + if self.stop_loss is not None: + if self.is_short: + self.stop_loss = min(self.stop_loss, liquidation_price) + else: + self.stop_loss = max(self.stop_loss, liquidation_price) + else: + self.initial_stop_loss = liquidation_price + self.stop_loss = liquidation_price + + self.liquidation_price = liquidation_price def __repr__(self): open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed' diff --git a/mkdocs.yml b/mkdocs.yml index 854939ca0..59f2bae73 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,61 +3,62 @@ site_url: https://www.freqtrade.io/ repo_url: https://github.com/freqtrade/freqtrade use_directory_urls: True nav: - - Home: index.md - - Quickstart with Docker: docker_quickstart.md - - Installation: - - Linux/MacOS/Raspberry: installation.md - - Windows: windows_installation.md - - Freqtrade Basics: bot-basics.md - - Configuration: configuration.md - - Strategy Customization: strategy-customization.md - - Plugins: plugins.md - - Stoploss: stoploss.md - - Start the bot: bot-usage.md - - Control the bot: - - Telegram: telegram-usage.md - - REST API & FreqUI: rest-api.md - - Web Hook: webhook-config.md - - Data Downloading: data-download.md - - Backtesting: backtesting.md - - Hyperopt: hyperopt.md - - Utility Sub-commands: utils.md - - Plotting: plotting.md - - Data Analysis: - - Jupyter Notebooks: data-analysis.md - - Strategy analysis: strategy_analysis_example.md - - Exchange-specific Notes: exchanges.md - - Advanced Topics: - - Advanced Post-installation Tasks: advanced-setup.md - - Edge Positioning: edge.md - - Advanced Strategy: strategy-advanced.md - - Advanced Hyperopt: advanced-hyperopt.md - - Sandbox Testing: sandbox-testing.md - - FAQ: faq.md - - SQL Cheat-sheet: sql_cheatsheet.md - - Updating Freqtrade: updating.md - - Deprecated Features: deprecated.md - - Contributors Guide: developer.md + - Home: index.md + - Quickstart with Docker: docker_quickstart.md + - Installation: + - Linux/MacOS/Raspberry: installation.md + - Windows: windows_installation.md + - Freqtrade Basics: bot-basics.md + - Configuration: configuration.md + - Strategy Customization: strategy-customization.md + - Plugins: plugins.md + - Stoploss: stoploss.md + - Start the bot: bot-usage.md + - Control the bot: + - Telegram: telegram-usage.md + - REST API & FreqUI: rest-api.md + - Web Hook: webhook-config.md + - Data Downloading: data-download.md + - Backtesting: backtesting.md + - Leverage: leverage.md + - Hyperopt: hyperopt.md + - Utility Sub-commands: utils.md + - Plotting: plotting.md + - Data Analysis: + - Jupyter Notebooks: data-analysis.md + - Strategy analysis: strategy_analysis_example.md + - Exchange-specific Notes: exchanges.md + - Advanced Topics: + - Advanced Post-installation Tasks: advanced-setup.md + - Edge Positioning: edge.md + - Advanced Strategy: strategy-advanced.md + - Advanced Hyperopt: advanced-hyperopt.md + - Sandbox Testing: sandbox-testing.md + - FAQ: faq.md + - SQL Cheat-sheet: sql_cheatsheet.md + - Updating Freqtrade: updating.md + - Deprecated Features: deprecated.md + - Contributors Guide: developer.md theme: name: material - logo: 'images/logo.png' - favicon: 'images/logo.png' - custom_dir: 'docs/overrides' + logo: "images/logo.png" + favicon: "images/logo.png" + custom_dir: "docs/overrides" palette: - scheme: default - primary: 'blue grey' - accent: 'tear' + primary: "blue grey" + accent: "tear" toggle: icon: material/toggle-switch-off-outline name: Switch to dark mode - scheme: slate - primary: 'blue grey' - accent: 'tear' + primary: "blue grey" + accent: "tear" toggle: icon: material/toggle-switch-off-outline name: Switch to dark mode extra_css: - - 'stylesheets/ft.extra.css' + - "stylesheets/ft.extra.css" extra_javascript: - javascripts/config.js - https://polyfill.io/v3/polyfill.min.js?features=es6 diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 00ffd3fe4..226c49305 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -418,6 +418,7 @@ def leverage_order_sell(): 'amount': 123.0, 'filled': 123.0, 'remaining': 0.0, + 'leverage': 5.0 } diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 4fc979568..cf1ed0121 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -105,6 +105,85 @@ def test_is_opening_closing_trade(fee): assert trade.is_closing_trade('sell') is False +@pytest.mark.usefixtures("init_persistence") +def test_set_stop_loss_liquidation_price(fee): + trade = Trade( + id=2, + pair='ETH/BTC', + stake_amount=0.001, + open_rate=0.01, + amount=5, + is_open=True, + open_date=arrow.utcnow().datetime, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + is_short=False, + leverage=2.0 + ) + trade.set_liquidation_price(0.09) + assert trade.liquidation_price == 0.09 + assert trade.stop_loss == 0.09 + assert trade.initial_stop_loss == 0.09 + + trade.set_stop_loss(0.1) + assert trade.liquidation_price == 0.09 + assert trade.stop_loss == 0.1 + assert trade.initial_stop_loss == 0.09 + + trade.set_liquidation_price(0.08) + assert trade.liquidation_price == 0.08 + assert trade.stop_loss == 0.1 + assert trade.initial_stop_loss == 0.09 + + trade.set_liquidation_price(0.11) + assert trade.liquidation_price == 0.11 + assert trade.stop_loss == 0.11 + assert trade.initial_stop_loss == 0.09 + + trade.set_stop_loss(0.1) + assert trade.liquidation_price == 0.11 + assert trade.stop_loss == 0.11 + assert trade.initial_stop_loss == 0.09 + + trade.stop_loss = None + trade.liquidation_price = None + trade.initial_stop_loss = None + trade.set_stop_loss(0.07) + assert trade.liquidation_price is None + assert trade.stop_loss == 0.07 + assert trade.initial_stop_loss == 0.07 + + trade.is_short = True + trade.stop_loss = None + trade.initial_stop_loss = None + + trade.set_liquidation_price(0.09) + assert trade.liquidation_price == 0.09 + assert trade.stop_loss == 0.09 + assert trade.initial_stop_loss == 0.09 + + trade.set_stop_loss(0.08) + assert trade.liquidation_price == 0.09 + assert trade.stop_loss == 0.08 + assert trade.initial_stop_loss == 0.09 + + trade.set_liquidation_price(0.1) + assert trade.liquidation_price == 0.1 + assert trade.stop_loss == 0.08 + assert trade.initial_stop_loss == 0.09 + + trade.set_liquidation_price(0.07) + assert trade.liquidation_price == 0.07 + assert trade.stop_loss == 0.07 + assert trade.initial_stop_loss == 0.09 + + trade.set_stop_loss(0.1) + assert trade.liquidation_price == 0.07 + assert trade.stop_loss == 0.07 + assert trade.initial_stop_loss == 0.09 + + @pytest.mark.usefixtures("init_persistence") def test_update_with_binance(limit_buy_order, limit_sell_order, fee, caplog): """ @@ -729,7 +808,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert orders[1].order_id == 'stop_order_id222' assert orders[1].ft_order_side == 'stoploss' - assert orders[0].is_short is False + # assert orders[0].is_short is False def test_migrate_mid_state(mocker, default_conf, fee, caplog): From 9a03cae920fd433c0f80be0f85253cb665972a4d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 11 Jul 2021 11:08:05 +0200 Subject: [PATCH 40/46] Try fix migration tests --- freqtrade/persistence/migrations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index c9fa4259b..77254a9a6 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -51,7 +51,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col leverage = get_column_def(cols, 'leverage', '1.0') interest_rate = get_column_def(cols, 'interest_rate', '0.0') liquidation_price = get_column_def(cols, 'liquidation_price', 'null') - is_short = get_column_def(cols, 'is_short', 'False') + # sqlite does not support literals for booleans + is_short = get_column_def(cols, 'is_short', '0') interest_mode = get_column_def(cols, 'interest_mode', 'null') # If ticker-interval existed use that, else null. if has_column(cols, 'ticker_interval'): From 3d7a74551fbb75758880a7f09734c73413ecf90a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 11 Jul 2021 11:16:46 +0200 Subject: [PATCH 41/46] Boolean sqlite fix for orders table --- freqtrade/persistence/migrations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 77254a9a6..3a3457354 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -150,8 +150,9 @@ def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, col # let SQLAlchemy create the schema as required decl_base.metadata.create_all(engine) leverage = get_column_def(cols, 'leverage', '1.0') - # is_short = get_column_def(cols, 'is_short', 'False') - + # sqlite does not support literals for booleans + is_short = get_column_def(cols, 'is_short', '0') + # TODO-mg: Should liquidation price go in here? with engine.begin() as connection: connection.execute(text(f""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, From 4b81fb31fbf47b6c50cc61fe80320cd09f90be80 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 13 Jul 2021 22:54:33 -0600 Subject: [PATCH 42/46] Changed the name of a test to match it's equivelent Removed test-analysis-lev --- tests/persistence/test_persistence_leverage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/persistence/test_persistence_leverage.py b/tests/persistence/test_persistence_leverage.py index a5b5178d1..da1cbd265 100644 --- a/tests/persistence/test_persistence_leverage.py +++ b/tests/persistence/test_persistence_leverage.py @@ -372,7 +372,7 @@ def test_calc_close_trade_price_lev(market_lev_buy_order, market_lev_sell_order, @pytest.mark.usefixtures("init_persistence") -def test_update_limit_order_lev(limit_lev_buy_order, limit_lev_sell_order, fee, caplog): +def test_update_with_binance_lev(limit_lev_buy_order, limit_lev_sell_order, fee, caplog): """ 10 minute leveraged limit trade on binance at 3x leverage From 35fd8d6a021425ea719d5b9b98e6bee3b184edfa Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 17 Jul 2021 01:57:57 -0600 Subject: [PATCH 43/46] Added enter_side and exit_side computed variables to persistence --- freqtrade/persistence/models.py | 14 ++++++++++++++ tests/persistence/test_persistence.py | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 69a103123..3500b9c8a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -297,6 +297,20 @@ class LocalTrade(): def close_date_utc(self): return self.close_date.replace(tzinfo=timezone.utc) + @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" + def __init__(self, **kwargs): for key in kwargs: setattr(self, key, kwargs[key]) diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index cf1ed0121..913a40ca1 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -83,6 +83,8 @@ def test_is_opening_closing_trade(fee): assert trade.is_opening_trade('sell') is False assert trade.is_closing_trade('buy') is False assert trade.is_closing_trade('sell') is True + assert trade.enter_side == 'buy' + assert trade.exit_side == 'sell' trade = Trade( id=2, @@ -103,6 +105,8 @@ def test_is_opening_closing_trade(fee): assert trade.is_opening_trade('sell') is True assert trade.is_closing_trade('buy') is True assert trade.is_closing_trade('sell') is False + assert trade.enter_side == 'sell' + assert trade.exit_side == 'buy' @pytest.mark.usefixtures("init_persistence") From 1918304c5b05ccb0fa12586d37d19302d9e29260 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 20 Jul 2021 17:56:57 -0600 Subject: [PATCH 44/46] persistence all to one test file, use more regular values like 2.0 for persistence tests --- freqtrade/persistence/migrations.py | 2 - freqtrade/persistence/models.py | 13 +- tests/conftest.py | 161 +-- tests/conftest_trades.py | 10 +- .../persistence/test_persistence_leverage.py | 638 ---------- tests/persistence/test_persistence_short.py | 780 ------------ tests/{persistence => }/test_persistence.py | 1051 ++++++++++++++--- 7 files changed, 963 insertions(+), 1692 deletions(-) delete mode 100644 tests/persistence/test_persistence_leverage.py delete mode 100644 tests/persistence/test_persistence_short.py rename tests/{persistence => }/test_persistence.py (51%) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 3a3457354..39997a8f4 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -151,8 +151,6 @@ def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, col decl_base.metadata.create_all(engine) leverage = get_column_def(cols, 'leverage', '1.0') # sqlite does not support literals for booleans - is_short = get_column_def(cols, 'is_short', '0') - # TODO-mg: Should liquidation price go in here? with engine.begin() as connection: connection.execute(text(f""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 3500b9c8a..9e2e99063 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -236,7 +236,7 @@ class LocalTrade(): close_rate_requested: Optional[float] = None close_profit: Optional[float] = None close_profit_abs: Optional[float] = None - stake_amount: float = 0.0 + stake_amount: float = 0.0 # TODO: This should probably be computed amount: float = 0.0 amount_requested: Optional[float] = None open_date: datetime @@ -273,7 +273,7 @@ class LocalTrade(): @property def has_no_leverage(self) -> bool: """Returns true if this is a non-leverage, non-short trade""" - return (self.leverage == 1.0 and not self.is_short) or self.leverage is None + return ((self.leverage or self.leverage is None) == 1.0 and not self.is_short) @property def borrowed(self) -> float: @@ -285,7 +285,7 @@ class LocalTrade(): if self.has_no_leverage: return 0.0 elif not self.is_short: - return self.stake_amount * (self.leverage-1) + return (self.amount * self.open_rate) * ((self.leverage-1)/self.leverage) else: return self.amount @@ -351,6 +351,10 @@ class LocalTrade(): self.liquidation_price = liquidation_price + def set_is_short(self, is_short: bool): + self.is_short = is_short + self.recalc_open_trade_value() + def __repr__(self): open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed' @@ -635,7 +639,8 @@ class LocalTrade(): def recalc_open_trade_value(self) -> None: """ Recalculate open_trade_value. - Must be called whenever open_rate or fee_open is changed. + Must be called whenever open_rate, fee_open or is_short is changed. + """ self.open_trade_value = self._calc_open_trade_value() diff --git a/tests/conftest.py b/tests/conftest.py index eb0c14a45..2b0aee336 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -205,16 +205,22 @@ def create_mock_trades(fee, use_db: bool = True): # Simulate dry_run entries trade = mock_trade_1(fee) add_trade(trade) + trade = mock_trade_2(fee) add_trade(trade) + trade = mock_trade_3(fee) add_trade(trade) + trade = mock_trade_4(fee) add_trade(trade) + trade = mock_trade_5(fee) add_trade(trade) + trade = mock_trade_6(fee) add_trade(trade) + if use_db: Trade.query.session.flush() @@ -231,6 +237,7 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True): # Simulate dry_run entries trade = mock_trade_1(fee) add_trade(trade) + trade = mock_trade_2(fee) add_trade(trade) @@ -248,6 +255,7 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True): trade = short_trade(fee) add_trade(trade) + trade = leverage_trade(fee) add_trade(trade) if use_db: @@ -2111,105 +2119,12 @@ def saved_hyperopt_results(): for res in hyperopt_res: res['results_metrics']['holding_avg_s'] = res['results_metrics']['holding_avg' ].total_seconds() + return hyperopt_res -# * Margin Tests - @pytest.fixture(scope='function') -def limit_short_order_open(): - return { - 'id': 'mocked_limit_short', - 'type': 'limit', - 'side': 'sell', - 'symbol': 'mocked', - 'datetime': arrow.utcnow().isoformat(), - 'timestamp': arrow.utcnow().int_timestamp, - 'price': 0.00001173, - 'amount': 90.99181073, - 'leverage': 1.0, - 'filled': 0.0, - 'cost': 0.00106733393, - 'remaining': 90.99181073, - 'status': 'open', - 'is_short': True - } - - -@pytest.fixture -def limit_exit_short_order_open(): - return { - 'id': 'mocked_limit_exit_short', - 'type': 'limit', - 'side': 'buy', - 'pair': 'mocked', - 'datetime': arrow.utcnow().isoformat(), - 'timestamp': arrow.utcnow().int_timestamp, - 'price': 0.00001099, - 'amount': 90.99370639272354, - 'filled': 0.0, - 'remaining': 90.99370639272354, - 'status': 'open', - 'leverage': 1.0 - } - - -@pytest.fixture(scope='function') -def limit_short_order(limit_short_order_open): - order = deepcopy(limit_short_order_open) - order['status'] = 'closed' - order['filled'] = order['amount'] - order['remaining'] = 0.0 - return order - - -@pytest.fixture -def limit_exit_short_order(limit_exit_short_order_open): - order = deepcopy(limit_exit_short_order_open) - order['remaining'] = 0.0 - order['filled'] = order['amount'] - order['status'] = 'closed' - return order - - -@pytest.fixture(scope='function') -def market_short_order(): - return { - 'id': 'mocked_market_short', - 'type': 'market', - 'side': 'sell', - 'symbol': 'mocked', - 'datetime': arrow.utcnow().isoformat(), - 'price': 0.00004173, - 'amount': 275.97543219, - 'filled': 275.97543219, - 'remaining': 0.0, - 'status': 'closed', - 'is_short': True, - 'leverage': 3.0, - } - - -@pytest.fixture -def market_exit_short_order(): - return { - 'id': 'mocked_limit_exit_short', - 'type': 'market', - 'side': 'buy', - 'symbol': 'mocked', - 'datetime': arrow.utcnow().isoformat(), - 'price': 0.00004099, - 'amount': 276.113419906095, - 'filled': 276.113419906095, - 'remaining': 0.0, - 'status': 'closed', - 'leverage': 3.0 - } - - -# leverage 3x -@pytest.fixture(scope='function') -def limit_lev_buy_order_open(): +def limit_buy_order_usdt_open(): return { 'id': 'mocked_limit_buy', 'type': 'limit', @@ -2217,20 +2132,18 @@ def limit_lev_buy_order_open(): 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), 'timestamp': arrow.utcnow().int_timestamp, - 'price': 0.00001099, - 'amount': 272.97543219, + 'price': 2.00, + 'amount': 30.0, 'filled': 0.0, - 'cost': 0.0009999999999226999, - 'remaining': 272.97543219, - 'leverage': 3.0, - 'status': 'open', - 'exchange': 'binance', + 'cost': 60.0, + 'remaining': 30.0, + 'status': 'open' } @pytest.fixture(scope='function') -def limit_lev_buy_order(limit_lev_buy_order_open): - order = deepcopy(limit_lev_buy_order_open) +def limit_buy_order_usdt(limit_buy_order_usdt_open): + order = deepcopy(limit_buy_order_usdt_open) order['status'] = 'closed' order['filled'] = order['amount'] order['remaining'] = 0.0 @@ -2238,7 +2151,7 @@ def limit_lev_buy_order(limit_lev_buy_order_open): @pytest.fixture -def limit_lev_sell_order_open(): +def limit_sell_order_usdt_open(): return { 'id': 'mocked_limit_sell', 'type': 'limit', @@ -2246,19 +2159,17 @@ def limit_lev_sell_order_open(): 'pair': 'mocked', 'datetime': arrow.utcnow().isoformat(), 'timestamp': arrow.utcnow().int_timestamp, - 'price': 0.00001173, - 'amount': 272.97543219, + 'price': 2.20, + 'amount': 30.0, 'filled': 0.0, - 'remaining': 272.97543219, - 'leverage': 3.0, - 'status': 'open', - 'exchange': 'binance' + 'remaining': 30.0, + 'status': 'open' } @pytest.fixture -def limit_lev_sell_order(limit_lev_sell_order_open): - order = deepcopy(limit_lev_sell_order_open) +def limit_sell_order_usdt(limit_sell_order_usdt_open): + order = deepcopy(limit_sell_order_usdt_open) order['remaining'] = 0.0 order['filled'] = order['amount'] order['status'] = 'closed' @@ -2266,36 +2177,32 @@ def limit_lev_sell_order(limit_lev_sell_order_open): @pytest.fixture(scope='function') -def market_lev_buy_order(): +def market_buy_order_usdt(): return { 'id': 'mocked_market_buy', 'type': 'market', 'side': 'buy', 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), - 'price': 0.00004099, - 'amount': 275.97543219, - 'filled': 275.97543219, + 'price': 2.00, + 'amount': 30.0, + 'filled': 30.0, 'remaining': 0.0, - 'status': 'closed', - 'exchange': 'kraken', - 'leverage': 3.0 + 'status': 'closed' } @pytest.fixture -def market_lev_sell_order(): +def market_sell_order_usdt(): return { 'id': 'mocked_limit_sell', 'type': 'market', 'side': 'sell', 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), - 'price': 0.00004173, - 'amount': 275.97543219, - 'filled': 275.97543219, + 'price': 2.20, + 'amount': 30.0, + 'filled': 30.0, 'remaining': 0.0, - 'status': 'closed', - 'leverage': 3.0, - 'exchange': 'kraken' + 'status': 'closed' } diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 226c49305..cad6d195c 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta, timezone +from freqtrade.enums import InterestMode from freqtrade.persistence.models import Order, Trade @@ -382,8 +383,8 @@ def short_trade(fee): sell_reason='sell_signal', # TODO-mg: Update to exit/close reason open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), # close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2), - # borrowed= - is_short=True + is_short=True, + interest_mode=InterestMode.HOURSPERDAY ) o = Order.parse_from_ccxt_object(short_order(), 'ETC/BTC', 'sell') trade.orders.append(o) @@ -466,13 +467,14 @@ def leverage_trade(fee): close_profit_abs=2.5983135000000175, exchange='kraken', is_open=False, - open_order_id='dry_run_leverage_sell_12345', + open_order_id='dry_run_leverage_buy_12368', strategy='DefaultStrategy', timeframe=5, sell_reason='sell_signal', # TODO-mg: Update to exit/close reason open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=300), close_date=datetime.now(tz=timezone.utc), - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPER4 ) o = Order.parse_from_ccxt_object(leverage_order(), 'DOGE/BTC', 'sell') trade.orders.append(o) diff --git a/tests/persistence/test_persistence_leverage.py b/tests/persistence/test_persistence_leverage.py deleted file mode 100644 index da1cbd265..000000000 --- a/tests/persistence/test_persistence_leverage.py +++ /dev/null @@ -1,638 +0,0 @@ -from datetime import datetime, timedelta -from math import isclose - -import pytest - -from freqtrade.enums import InterestMode -from freqtrade.persistence import Trade -from tests.conftest import log_has_re - - -@pytest.mark.usefixtures("init_persistence") -def test_interest_kraken_lev(market_lev_buy_order, fee): - """ - Market trade on Kraken at 3x and 5x leverage - Short trade - interest_rate: 0.05%, 0.25% per 4 hrs - open_rate: 0.00004099 base - close_rate: 0.00004173 base - stake_amount: 0.0037707443218227 - borrowed: 0.0075414886436454 - amount: - 275.97543219 crypto - 459.95905365 crypto - borrowed: - 0.0075414886436454 base - 0.0150829772872908 base - time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) - 5 hours = 5/4 - - interest: borrowed * interest_rate * ceil(1 + time-periods) - = 0.0075414886436454 * 0.0005 * ceil(2) = 7.5414886436454e-06 base - = 0.0075414886436454 * 0.00025 * ceil(9/4) = 5.65611648273405e-06 base - = 0.0150829772872908 * 0.0005 * ceil(9/4) = 2.26244659309362e-05 base - = 0.0150829772872908 * 0.00025 * ceil(2) = 7.5414886436454e-06 base - """ - - trade = Trade( - pair='ETH/BTC', - stake_amount=0.0037707443218227, - amount=275.97543219, - open_rate=0.00001099, - open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='kraken', - leverage=3.0, - interest_rate=0.0005, - interest_mode=InterestMode.HOURSPER4 - ) - - assert float(trade.calculate_interest()) == 7.5414886436454e-06 - trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55) - assert float(round(trade.calculate_interest(interest_rate=0.00025), 11) - ) == round(5.65611648273405e-06, 11) - - trade = Trade( - pair='ETH/BTC', - stake_amount=0.0037707443218227, - amount=459.95905365, - open_rate=0.00001099, - open_date=datetime.utcnow() - timedelta(hours=4, minutes=55), - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='kraken', - leverage=5.0, - interest_rate=0.0005, - interest_mode=InterestMode.HOURSPER4 - ) - - assert float(round(trade.calculate_interest(), 11) - ) == round(2.26244659309362e-05, 11) - trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10) - trade.interest_rate = 0.00025 - assert float(trade.calculate_interest(interest_rate=0.00025)) == 7.5414886436454e-06 - - -@pytest.mark.usefixtures("init_persistence") -def test_interest_binance_lev(market_lev_buy_order, fee): - """ - Market trade on Kraken at 3x and 5x leverage - Short trade - interest_rate: 0.05%, 0.25% per 4 hrs - open_rate: 0.00001099 base - close_rate: 0.00001173 base - stake_amount: 0.0009999999999226999 - borrowed: 0.0019999999998453998 - amount: - 90.99181073 * leverage(3) = 272.97543219 crypto - 90.99181073 * leverage(5) = 454.95905365 crypto - borrowed: - 0.0019999999998453998 base - 0.0039999999996907995 base - time-periods: 10 minutes(rounds up to 1/24 time-period of 24hrs) - 5 hours = 5/24 - - interest: borrowed * interest_rate * time-periods - = 0.0019999999998453998 * 0.00050 * 1/24 = 4.166666666344583e-08 base - = 0.0019999999998453998 * 0.00025 * 5/24 = 1.0416666665861459e-07 base - = 0.0039999999996907995 * 0.00050 * 5/24 = 4.1666666663445834e-07 base - = 0.0039999999996907995 * 0.00025 * 1/24 = 4.166666666344583e-08 base - """ - - trade = Trade( - pair='ETH/BTC', - stake_amount=0.0009999999999226999, - amount=272.97543219, - open_rate=0.00001099, - open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - leverage=3.0, - interest_rate=0.0005, - interest_mode=InterestMode.HOURSPERDAY - ) - # 10 minutes round up to 4 hours evenly on kraken so we can predict the them more accurately - assert round(float(trade.calculate_interest()), 22) == round(4.166666666344583e-08, 22) - trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55) - # All trade > 5 hours will vary slightly due to execution time and interest calculated - assert float(round(trade.calculate_interest(interest_rate=0.00025), 14) - ) == round(1.0416666665861459e-07, 14) - - trade = Trade( - pair='ETH/BTC', - stake_amount=0.0009999999999226999, - amount=459.95905365, - open_rate=0.00001099, - open_date=datetime.utcnow() - timedelta(hours=4, minutes=55), - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - leverage=5.0, - interest_rate=0.0005, - interest_mode=InterestMode.HOURSPERDAY - ) - - assert float(round(trade.calculate_interest(), 14)) == round(4.1666666663445834e-07, 14) - trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10) - assert float(round(trade.calculate_interest(interest_rate=0.00025), 22) - ) == round(4.166666666344583e-08, 22) - - -@pytest.mark.usefixtures("init_persistence") -def test_update_open_order_lev(limit_lev_buy_order): - trade = Trade( - pair='ETH/BTC', - stake_amount=1.00, - open_rate=0.01, - amount=5, - fee_open=0.1, - fee_close=0.1, - interest_rate=0.0005, - exchange='binance', - interest_mode=InterestMode.HOURSPERDAY - ) - assert trade.open_order_id is None - assert trade.close_profit is None - assert trade.close_date is None - limit_lev_buy_order['status'] = 'open' - trade.update(limit_lev_buy_order) - assert trade.open_order_id is None - assert trade.close_profit is None - assert trade.close_date is None - - -@pytest.mark.usefixtures("init_persistence") -def test_calc_open_trade_value_lev(market_lev_buy_order, fee): - """ - 10 minute leveraged market trade on Kraken at 3x leverage - Short trade - fee: 0.25% base - interest_rate: 0.05% per 4 hrs - open_rate: 0.00004099 base - close_rate: 0.00004173 base - amount: 91.99181073 * leverage(3) = 275.97543219 crypto - stake_amount: 0.0037707443218227 - borrowed: 0.0075414886436454 base - time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) - interest: borrowed * interest_rate * time-periods - = 0.0075414886436454 * 0.0005 * 1 = 7.5414886436454e-06 crypto - open_value: (amount * open_rate) + (amount * open_rate * fee) - = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) - = 0.01134051354788177 - """ - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=5, - open_rate=0.00004099, - open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), - fee_open=fee.return_value, - fee_close=fee.return_value, - interest_rate=0.0005, - exchange='kraken', - leverage=3, - interest_mode=InterestMode.HOURSPER4 - ) - trade.open_order_id = 'open_trade' - trade.update(market_lev_buy_order) # Buy @ 0.00001099 - # Get the open rate price with the standard fee rate - assert trade._calc_open_trade_value() == 0.01134051354788177 - trade.fee_open = 0.003 - # Get the open rate price with a custom fee rate - assert trade._calc_open_trade_value() == 0.011346169664364504 - - -@pytest.mark.usefixtures("init_persistence") -def test_calc_open_close_trade_price_lev(limit_lev_buy_order, limit_lev_sell_order, fee): - """ - 5 hour leveraged trade on Binance - - fee: 0.25% base - interest_rate: 0.05% per day - open_rate: 0.00001099 base - close_rate: 0.00001173 base - amount: 272.97543219 crypto - stake_amount: 0.0009999999999226999 base - borrowed: 0.0019999999998453998 base - time-periods: 5 hours(rounds up to 5/24 time-period of 1 day) - interest: borrowed * interest_rate * time-periods - = 0.0019999999998453998 * 0.0005 * 5/24 = 2.0833333331722917e-07 base - open_value: (amount * open_rate) + (amount * open_rate * fee) - = (272.97543219 * 0.00001099) + (272.97543219 * 0.00001099 * 0.0025) - = 0.0030074999997675204 - close_value: ((amount_closed * close_rate) - (amount_closed * close_rate * fee)) - interest - = (272.97543219 * 0.00001173) - - (272.97543219 * 0.00001173 * 0.0025) - - 2.0833333331722917e-07 - = 0.003193788481706411 - stake_value: (amount/lev * open_rate) + (amount/lev * open_rate * fee) - = (272.97543219/3 * 0.00001099) + (272.97543219/3 * 0.00001099 * 0.0025) - = 0.0010024999999225066 - total_profit = close_value - open_value - = 0.003193788481706411 - 0.0030074999997675204 - = 0.00018628848193889044 - total_profit_percentage = total_profit / stake_value - = 0.00018628848193889054 / 0.0010024999999225066 - = 0.18582392214792087 - """ - trade = Trade( - pair='ETH/BTC', - stake_amount=0.0009999999999226999, - open_rate=0.01, - amount=5, - open_date=datetime.utcnow() - timedelta(hours=4, minutes=55), - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - interest_rate=0.0005, - interest_mode=InterestMode.HOURSPERDAY - ) - trade.open_order_id = 'something' - trade.update(limit_lev_buy_order) - assert trade._calc_open_trade_value() == 0.00300749999976752 - trade.update(limit_lev_sell_order) - - # Is slightly different due to compilation time changes. Interest depends on time - assert round(trade.calc_close_trade_value(), 11) == round(0.003193788481706411, 11) - # Profit in BTC - assert round(trade.calc_profit(), 8) == round(0.00018628848193889054, 8) - # Profit in percent - assert round(trade.calc_profit_ratio(), 8) == round(0.18582392214792087, 8) - - -@pytest.mark.usefixtures("init_persistence") -def test_trade_close_lev(fee): - """ - 5 hour leveraged market trade on Kraken at 3x leverage - fee: 0.25% base - interest_rate: 0.05% per 4 hrs - open_rate: 0.1 base - close_rate: 0.2 base - amount: 5 * leverage(3) = 15 crypto - stake_amount: 0.5 - borrowed: 1 base - time-periods: 5/4 periods of 4hrs - interest: borrowed * interest_rate * ceil(1 + time-periods) - = 1 * 0.0005 * ceil(9/4) = 0.0015 crypto - open_value: (amount * open_rate) + (amount * open_rate * fee) - = (15 * 0.1) + (15 * 0.1 * 0.0025) - = 1.50375 - close_value: (amount * close_rate) + (amount * close_rate * fee) - interest - = (15 * 0.2) - (15 * 0.2 * 0.0025) - 0.0015 - = 2.991 - total_profit = close_value - open_value - = 2.991 - 1.50375 - = 1.4872500000000002 - total_profit_ratio = ((close_value/open_value) - 1) * leverage - = ((2.991/1.50375) - 1) * 3 - = 2.96708229426434 - """ - trade = Trade( - pair='ETH/BTC', - stake_amount=0.5, - open_rate=0.1, - amount=15, - is_open=True, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_date=datetime.utcnow() - timedelta(hours=4, minutes=55), - exchange='kraken', - leverage=3.0, - interest_rate=0.0005, - interest_mode=InterestMode.HOURSPER4 - ) - assert trade.close_profit is None - assert trade.close_date is None - assert trade.is_open is True - trade.close(0.2) - assert trade.is_open is False - assert trade.close_profit == round(2.96708229426434, 8) - assert trade.close_date is not None - - # TODO-mg: Remove these comments probably - # new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, - # assert trade.close_date != new_date - # # Close should NOT update close_date if the trade has been closed already - # assert trade.is_open is False - # trade.close_date = new_date - # trade.close(0.02) - # assert trade.close_date == new_date - - -@pytest.mark.usefixtures("init_persistence") -def test_calc_close_trade_price_lev(market_lev_buy_order, market_lev_sell_order, fee): - """ - 10 minute leveraged market trade on Kraken at 3x leverage - Short trade - fee: 0.25% base - interest_rate: 0.05% per 4 hrs - open_rate: 0.00004099 base - close_rate: 0.00004173 base - amount: 91.99181073 * leverage(3) = 275.97543219 crypto - stake_amount: 0.0037707443218227 - borrowed: 0.0075414886436454 base - time-periods: 10 minutes = 2 - interest: borrowed * interest_rate * time-periods - = 0.0075414886436454 * 0.0005 * 2 = 7.5414886436454e-06 crypto - open_value: (amount * open_rate) + (amount * open_rate * fee) - = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) - = 0.01134051354788177 - close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest - = (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.0025) - 7.5414886436454e-06 - = 0.0033894815024978933 - = (275.97543219 * 0.00001234) - (275.97543219 * 0.00001234 * 0.003) - 7.5414886436454e-06 - = 0.003387778734081281 - = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.005) - 7.5414886436454e-06 - = 0.011451331022718612 - """ - trade = Trade( - pair='ETH/BTC', - stake_amount=0.0037707443218227, - amount=5, - open_rate=0.00004099, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), - interest_rate=0.0005, - leverage=3.0, - exchange='kraken', - interest_mode=InterestMode.HOURSPER4 - ) - trade.open_order_id = 'close_trade' - trade.update(market_lev_buy_order) # Buy @ 0.00001099 - # Get the close rate price with a custom close rate and a regular fee rate - assert isclose(trade.calc_close_trade_value(rate=0.00001234), 0.0033894815024978933) - # Get the close rate price with a custom close rate and a custom fee rate - assert isclose(trade.calc_close_trade_value(rate=0.00001234, fee=0.003), 0.003387778734081281) - # Test when we apply a Sell order, and ask price with a custom fee rate - trade.update(market_lev_sell_order) - assert isclose(trade.calc_close_trade_value(fee=0.005), 0.011451331022718612) - - -@pytest.mark.usefixtures("init_persistence") -def test_update_with_binance_lev(limit_lev_buy_order, limit_lev_sell_order, fee, caplog): - """ - 10 minute leveraged limit trade on binance at 3x leverage - - Leveraged trade - fee: 0.25% base - interest_rate: 0.05% per day - open_rate: 0.00001099 base - close_rate: 0.00001173 base - amount: 272.97543219 crypto - stake_amount: 0.0009999999999226999 base - borrowed: 0.0019999999998453998 base - time-periods: 10 minutes(rounds up to 1/24 time-period of 1 day) - interest: borrowed * interest_rate * time-periods - = 0.0019999999998453998 * 0.0005 * 1/24 = 4.166666666344583e-08 base - open_value: (amount * open_rate) + (amount * open_rate * fee) - = (272.97543219 * 0.00001099) + (272.97543219 * 0.00001099 * 0.0025) - = 0.0030074999997675204 - stake_value = (amount/lev * open_rate) + (amount/lev * open_rate * fee) - = (272.97543219 * 0.00001099) + (272.97543219 * 0.00001099 * 0.0025) - = 0.0010024999999225066 - close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - = (272.97543219 * 0.00001173) - (272.97543219 * 0.00001173 * 0.0025) - = 0.003193996815039728 - stake_value: (amount/lev * open_rate) + (amount/lev * open_rate * fee) - = (272.97543219/3 * 0.00001099) + (272.97543219/3 * 0.00001099 * 0.0025) - = 0.0010024999999225066 - total_profit = close_value - open_value - interest - = 0.003193996815039728 - 0.0030074999997675204 - 4.166666666344583e-08 - = 0.00018645514860554435 - total_profit_percentage = total_profit / stake_value - = 0.00018645514860554435 / 0.0010024999999225066 - = 0.1859901731869899 - - """ - trade = Trade( - id=2, - pair='ETH/BTC', - stake_amount=0.0009999999999226999, - open_rate=0.01, - amount=5, - is_open=True, - open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), - fee_open=fee.return_value, - fee_close=fee.return_value, - interest_rate=0.0005, - exchange='binance', - interest_mode=InterestMode.HOURSPERDAY - ) - # assert trade.open_order_id is None - assert trade.close_profit is None - assert trade.close_date is None - - # trade.open_order_id = 'something' - trade.update(limit_lev_buy_order) - # assert trade.open_order_id is None - assert trade.open_rate == 0.00001099 - assert trade.close_profit is None - assert trade.close_date is None - assert trade.borrowed == 0.0019999999998453998 - assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, " - r"pair=ETH/BTC, amount=272.97543219, open_rate=0.00001099, open_since=.*\).", - caplog) - caplog.clear() - # trade.open_order_id = 'something' - trade.update(limit_lev_sell_order) - # assert trade.open_order_id is None - assert trade.close_rate == 0.00001173 - assert trade.close_profit == round(0.1859901731869899, 8) - assert trade.close_date is not None - assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, " - r"pair=ETH/BTC, amount=272.97543219, open_rate=0.00001099, open_since=.*\).", - caplog) - - -@pytest.mark.usefixtures("init_persistence") -def test_update_market_order_lev(market_lev_buy_order, market_lev_sell_order, fee, caplog): - """ - 10 minute leveraged market trade on Kraken at 3x leverage - Short trade - fee: 0.25% base - interest_rate: 0.05% per 4 hrs - open_rate: 0.00004099 base - close_rate: 0.00004173 base - amount: = 275.97543219 crypto - stake_amount: 0.0037707443218227 - borrowed: 0.0075414886436454 base - interest: borrowed * interest_rate * 1+ceil(hours) - = 0.0075414886436454 * 0.0005 * (1+ceil(1)) = 7.5414886436454e-06 crypto - open_value: (amount * open_rate) + (amount * open_rate * fee) - = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) - = 0.01134051354788177 - close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest - = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.0025) - 7.5414886436454e-06 - = 0.011480122159681833 - stake_value: (amount/lev * open_rate) + (amount/lev * open_rate * fee) - = (275.97543219/3 * 0.00004099) + (275.97543219/3 * 0.00004099 * 0.0025) - = 0.0037801711826272568 - total_profit = close_value - open_value - = 0.011480122159681833 - 0.01134051354788177 - = 0.00013960861180006392 - total_profit_percentage = ((close_value/open_value) - 1) * leverage - = ((0.011480122159681833 / 0.01134051354788177)-1) * 3 - = 0.036931822675563275 - """ - trade = Trade( - id=1, - pair='ETH/BTC', - stake_amount=0.0037707443218227, - amount=5, - open_rate=0.00004099, - is_open=True, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), - interest_rate=0.0005, - exchange='kraken', - interest_mode=InterestMode.HOURSPER4 - ) - trade.open_order_id = 'something' - trade.update(market_lev_buy_order) - assert trade.leverage == 3.0 - assert trade.open_order_id is None - assert trade.open_rate == 0.00004099 - assert trade.close_profit is None - assert trade.close_date is None - assert trade.interest_rate == 0.0005 - # TODO: Uncomment the next assert and make it work. - # The logger also has the exact same but there's some spacing in there - assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, " - r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004099, open_since=.*\).", - caplog) - caplog.clear() - trade.is_open = True - trade.open_order_id = 'something' - trade.update(market_lev_sell_order) - assert trade.open_order_id is None - assert trade.close_rate == 0.00004173 - assert trade.close_profit == round(0.036931822675563275, 8) - assert trade.close_date is not None - # TODO: The amount should maybe be the opening amount + the interest - # TODO: Uncomment the next assert and make it work. - # The logger also has the exact same but there's some spacing in there - assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " - r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004099, open_since=.*\).", - caplog) - - -@pytest.mark.usefixtures("init_persistence") -def test_calc_close_trade_price_exception_lev(limit_lev_buy_order, fee): - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - open_rate=0.1, - amount=5, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - interest_rate=0.0005, - leverage=3.0, - interest_mode=InterestMode.HOURSPERDAY - ) - trade.open_order_id = 'something' - trade.update(limit_lev_buy_order) - assert trade.calc_close_trade_value() == 0.0 - - -@pytest.mark.usefixtures("init_persistence") -def test_calc_profit_lev(market_lev_buy_order, market_lev_sell_order, fee): - """ - Leveraged trade on Kraken at 3x leverage - fee: 0.25% base or 0.3% - interest_rate: 0.05%, 0.25% per 4 hrs - open_rate: 0.00004099 base - close_rate: 0.00004173 base - stake_amount: 0.0037707443218227 - amount: 91.99181073 * leverage(3) = 275.97543219 crypto - borrowed: 0.0075414886436454 base - hours: 1/6, 5 hours - - interest: borrowed * interest_rate * ceil(1+hours/4) - = 0.0075414886436454 * 0.0005 * ceil(1+((1/6)/4)) = 7.5414886436454e-06 crypto - = 0.0075414886436454 * 0.00025 * ceil(1+(5/4)) = 5.65611648273405e-06 crypto - = 0.0075414886436454 * 0.0005 * ceil(1+(5/4)) = 1.13122329654681e-05 crypto - = 0.0075414886436454 * 0.00025 * ceil(1+((1/6)/4)) = 3.7707443218227e-06 crypto - open_value: (amount * open_rate) + (amount * open_rate * fee) - = (275.97543219 * 0.00004099) + (275.97543219 * 0.00004099 * 0.0025) - = 0.01134051354788177 - close_value: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest - (275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.0025) - 7.5414886436454e-06 - = 0.014786300937932227 - (275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.0025) - 5.65611648273405e-06 - = 0.0011973414905908902 - (275.97543219 * 0.00005374) - (275.97543219 * 0.00005374 * 0.003) - 1.13122329654681e-05 - = 0.01477511473374746 - (275.97543219 * 0.00000437) - (275.97543219 * 0.00000437 * 0.003) - 3.7707443218227e-06 - = 0.0011986238564324662 - stake_value: (amount/lev * open_rate) + (amount/lev * open_rate * fee) - = (275.97543219/3 * 0.00004099) + (275.97543219/3 * 0.00004099 * 0.0025) - = 0.0037801711826272568 - total_profit = close_value - open_value - = 0.014786300937932227 - 0.01134051354788177 = 0.0034457873900504577 - = 0.0011973414905908902 - 0.01134051354788177 = -0.01014317205729088 - = 0.01477511473374746 - 0.01134051354788177 = 0.00343460118586569 - = 0.0011986238564324662 - 0.01134051354788177 = -0.010141889691449303 - total_profit_percentage = ((close_value/open_value) - 1) * leverage - ((0.014786300937932227/0.01134051354788177) - 1) * 3 = 0.9115426851266561 - ((0.0011973414905908902/0.01134051354788177) - 1) * 3 = -2.683257336045103 - ((0.01477511473374746/0.01134051354788177) - 1) * 3 = 0.908583505860866 - ((0.0011986238564324662/0.01134051354788177) - 1) * 3 = -2.6829181011851926 - - """ - trade = Trade( - pair='ETH/BTC', - stake_amount=0.0037707443218227, - amount=5, - open_rate=0.00004099, - open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='kraken', - leverage=3.0, - interest_rate=0.0005, - interest_mode=InterestMode.HOURSPER4 - ) - trade.open_order_id = 'something' - trade.update(market_lev_buy_order) # Buy @ 0.00001099 - # Custom closing rate and regular fee rate - - # Higher than open rate - assert trade.calc_profit(rate=0.00005374, interest_rate=0.0005) == round( - 0.0034457873900504577, 8) - assert trade.calc_profit_ratio( - rate=0.00005374, interest_rate=0.0005) == round(0.9115426851266561, 8) - - # Lower than open rate - trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55) - assert trade.calc_profit( - rate=0.00000437, interest_rate=0.00025) == round(-0.01014317205729088, 8) - assert trade.calc_profit_ratio( - rate=0.00000437, interest_rate=0.00025) == round(-2.683257336045103, 8) - - # Custom closing rate and custom fee rate - # Higher than open rate - assert trade.calc_profit(rate=0.00005374, fee=0.003, - interest_rate=0.0005) == round(0.00343460118586569, 8) - assert trade.calc_profit_ratio(rate=0.00005374, fee=0.003, - interest_rate=0.0005) == round(0.908583505860866, 8) - - # Lower than open rate - trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10) - assert trade.calc_profit(rate=0.00000437, fee=0.003, - interest_rate=0.00025) == round(-0.010141889691449303, 8) - assert trade.calc_profit_ratio(rate=0.00000437, fee=0.003, - interest_rate=0.00025) == round(-2.6829181011851926, 8) - - # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 - trade.update(market_lev_sell_order) - assert trade.calc_profit() == round(0.00013960861180006392, 8) - assert trade.calc_profit_ratio() == round(0.036931822675563275, 8) - - # Test with a custom fee rate on the close trade - # assert trade.calc_profit(fee=0.003) == 0.00006163 - # assert trade.calc_profit_ratio(fee=0.003) == 0.06147824 diff --git a/tests/persistence/test_persistence_short.py b/tests/persistence/test_persistence_short.py deleted file mode 100644 index 2a1e46615..000000000 --- a/tests/persistence/test_persistence_short.py +++ /dev/null @@ -1,780 +0,0 @@ -from datetime import datetime, timedelta -from math import isclose - -import arrow -import pytest - -from freqtrade.enums import InterestMode -from freqtrade.persistence import Trade, init_db -from tests.conftest import create_mock_trades_with_leverage, log_has_re - - -@pytest.mark.usefixtures("init_persistence") -def test_interest_kraken_short(market_short_order, fee): - """ - Market trade on Kraken at 3x and 8x leverage - Short trade - interest_rate: 0.05%, 0.25% per 4 hrs - open_rate: 0.00004173 base - close_rate: 0.00004099 base - amount: - 275.97543219 crypto - 459.95905365 crypto - borrowed: - 275.97543219 crypto - 459.95905365 crypto - time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) - 5 hours = 5/4 - - interest: borrowed * interest_rate * ceil(1 + time-periods) - = 275.97543219 * 0.0005 * ceil(1+1) = 0.27597543219 crypto - = 275.97543219 * 0.00025 * ceil(9/4) = 0.20698157414249999 crypto - = 459.95905365 * 0.0005 * ceil(9/4) = 0.689938580475 crypto - = 459.95905365 * 0.00025 * ceil(1+1) = 0.229979526825 crypto - """ - - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=275.97543219, - open_rate=0.00001099, - open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='kraken', - is_short=True, - leverage=3.0, - interest_rate=0.0005, - interest_mode=InterestMode.HOURSPER4 - ) - - assert float(round(trade.calculate_interest(), 8)) == round(0.27597543219, 8) - trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55) - assert float(round(trade.calculate_interest(interest_rate=0.00025), 8) - ) == round(0.20698157414249999, 8) - - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=459.95905365, - open_rate=0.00001099, - open_date=datetime.utcnow() - timedelta(hours=4, minutes=55), - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='kraken', - is_short=True, - leverage=5.0, - interest_rate=0.0005, - interest_mode=InterestMode.HOURSPER4 - ) - - assert float(round(trade.calculate_interest(), 8)) == round(0.689938580475, 8) - trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10) - assert float(round(trade.calculate_interest(interest_rate=0.00025), 8) - ) == round(0.229979526825, 8) - - -@ pytest.mark.usefixtures("init_persistence") -def test_interest_binance_short(market_short_order, fee): - """ - Market trade on Binance at 3x and 5x leverage - Short trade - interest_rate: 0.05%, 0.25% per 1 day - open_rate: 0.00004173 base - close_rate: 0.00004099 base - amount: - 91.99181073 * leverage(3) = 275.97543219 crypto - 91.99181073 * leverage(5) = 459.95905365 crypto - borrowed: - 275.97543219 crypto - 459.95905365 crypto - time-periods: 10 minutes(rounds up to 1/24 time-period of 1 day) - 5 hours = 5/24 - - interest: borrowed * interest_rate * time-periods - = 275.97543219 * 0.0005 * 1/24 = 0.005749488170625 crypto - = 275.97543219 * 0.00025 * 5/24 = 0.0143737204265625 crypto - = 459.95905365 * 0.0005 * 5/24 = 0.047912401421875 crypto - = 459.95905365 * 0.00025 * 1/24 = 0.0047912401421875 crypto - """ - - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=275.97543219, - open_rate=0.00001099, - open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - is_short=True, - leverage=3.0, - interest_rate=0.0005, - interest_mode=InterestMode.HOURSPERDAY - ) - - assert float(round(trade.calculate_interest(), 8)) == 0.00574949 - trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55) - assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)) == 0.01437372 - - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=459.95905365, - open_rate=0.00001099, - open_date=datetime.utcnow() - timedelta(hours=4, minutes=55), - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - is_short=True, - leverage=5.0, - interest_rate=0.0005, - interest_mode=InterestMode.HOURSPERDAY - ) - - assert float(round(trade.calculate_interest(), 8)) == 0.04791240 - trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10) - assert float(round(trade.calculate_interest(interest_rate=0.00025), 8)) == 0.00479124 - - -@ pytest.mark.usefixtures("init_persistence") -def test_calc_open_trade_value_short(market_short_order, fee): - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=5, - open_rate=0.00004173, - open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), - fee_open=fee.return_value, - fee_close=fee.return_value, - interest_rate=0.0005, - is_short=True, - leverage=3.0, - exchange='kraken', - interest_mode=InterestMode.HOURSPER4 - ) - trade.open_order_id = 'open_trade' - trade.update(market_short_order) # Buy @ 0.00001099 - # Get the open rate price with the standard fee rate - assert trade._calc_open_trade_value() == 0.011487663648325479 - trade.fee_open = 0.003 - # Get the open rate price with a custom fee rate - assert trade._calc_open_trade_value() == 0.011481905420932834 - - -@ pytest.mark.usefixtures("init_persistence") -def test_update_open_order_short(limit_short_order): - trade = Trade( - pair='ETH/BTC', - stake_amount=1.00, - open_rate=0.01, - amount=5, - leverage=3.0, - fee_open=0.1, - fee_close=0.1, - interest_rate=0.0005, - is_short=True, - exchange='binance', - interest_mode=InterestMode.HOURSPERDAY - ) - assert trade.open_order_id is None - assert trade.close_profit is None - assert trade.close_date is None - limit_short_order['status'] = 'open' - trade.update(limit_short_order) - assert trade.open_order_id is None - assert trade.close_profit is None - assert trade.close_date is None - - -@ pytest.mark.usefixtures("init_persistence") -def test_calc_close_trade_price_exception_short(limit_short_order, fee): - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - open_rate=0.1, - amount=15.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - interest_rate=0.0005, - leverage=3.0, - is_short=True, - interest_mode=InterestMode.HOURSPERDAY - ) - trade.open_order_id = 'something' - trade.update(limit_short_order) - assert trade.calc_close_trade_value() == 0.0 - - -@ pytest.mark.usefixtures("init_persistence") -def test_calc_close_trade_price_short(market_short_order, market_exit_short_order, fee): - """ - 10 minute short market trade on Kraken at 3x leverage - Short trade - fee: 0.25% base - interest_rate: 0.05% per 4 hrs - open_rate: 0.00004173 base - close_rate: 0.00001234 base - amount: = 275.97543219 crypto - borrowed: 275.97543219 crypto - hours: 10 minutes = 1/6 - interest: borrowed * interest_rate * ceil(1 + hours/4) - = 275.97543219 * 0.0005 * ceil(1 + ((1/6)/4)) = 0.27597543219 crypto - amount_closed: amount + interest = 275.97543219 + 0.27597543219 = 276.25140762219 - close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - = (276.25140762219 * 0.00004099) + (276.25140762219 * 0.00004099 * 0.005) - = 0.011380162924425737 - """ - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=5, - open_rate=0.00001099, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), - interest_rate=0.0005, - is_short=True, - leverage=3.0, - exchange='kraken', - interest_mode=InterestMode.HOURSPER4 - ) - trade.open_order_id = 'close_trade' - trade.update(market_short_order) # Buy @ 0.00001099 - # Get the close rate price with a custom close rate and a regular fee rate - assert isclose(trade.calc_close_trade_value(rate=0.00001234), 0.0034174647259) - # Get the close rate price with a custom close rate and a custom fee rate - assert isclose(trade.calc_close_trade_value(rate=0.00001234, fee=0.003), 0.0034191691971679986) - # Test when we apply a Sell order, and ask price with a custom fee rate - trade.update(market_exit_short_order) - assert isclose(trade.calc_close_trade_value(fee=0.005), 0.011380162924425737) - - -@ pytest.mark.usefixtures("init_persistence") -def test_calc_open_close_trade_price_short(limit_short_order, limit_exit_short_order, fee): - """ - 5 hour short trade on Binance - Short trade - fee: 0.25% base - interest_rate: 0.05% per day - open_rate: 0.00001173 base - close_rate: 0.00001099 base - amount: 90.99181073 crypto - borrowed: 90.99181073 crypto - stake_amount: 0.0010673339398629 - time-periods: 5 hours = 5/24 - interest: borrowed * interest_rate * time-periods - = 90.99181073 * 0.0005 * 5/24 = 0.009478313617708333 crypto - open_value: (amount * open_rate) - (amount * open_rate * fee) - = (90.99181073 * 0.00001173) - (90.99181073 * 0.00001173 * 0.0025) - = 0.0010646656050132426 - amount_closed: amount + interest = 90.99181073 + 0.009478313617708333 = 91.0012890436177 - close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - = (91.0012890436177 * 0.00001099) + (91.0012890436177 * 0.00001099 * 0.0025) - = 0.001002604427005832 - stake_value = (amount/lev * open_rate) - (amount/lev * open_rate * fee) - = 0.0010646656050132426 - total_profit = open_value - close_value - = 0.0010646656050132426 - 0.001002604427005832 - = 0.00006206117800741065 - total_profit_percentage = (close_value - open_value) / stake_value - = (0.0010646656050132426 - 0.001002604427005832)/0.0010646656050132426 - = 0.05829170935473088 - """ - trade = Trade( - pair='ETH/BTC', - stake_amount=0.0010673339398629, - open_rate=0.01, - amount=5, - open_date=datetime.utcnow() - timedelta(hours=4, minutes=55), - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - interest_rate=0.0005, - interest_mode=InterestMode.HOURSPERDAY - ) - trade.open_order_id = 'something' - trade.update(limit_short_order) - assert trade._calc_open_trade_value() == 0.0010646656050132426 - trade.update(limit_exit_short_order) - - # Is slightly different due to compilation time. Interest depends on time - assert round(trade.calc_close_trade_value(), 11) == round(0.001002604427005832, 11) - # Profit in BTC - assert round(trade.calc_profit(), 8) == round(0.00006206117800741065, 8) - # Profit in percent - assert round(trade.calc_profit_ratio(), 8) == round(0.05829170935473088, 8) - - -@ pytest.mark.usefixtures("init_persistence") -def test_trade_close_short(fee): - """ - Five hour short trade on Kraken at 3x leverage - Short trade - Exchange: Kraken - fee: 0.25% base - interest_rate: 0.05% per 4 hours - open_rate: 0.02 base - close_rate: 0.01 base - leverage: 3.0 - amount: 15 crypto - borrowed: 15 crypto - time-periods: 5 hours = 5/4 - - interest: borrowed * interest_rate * time-periods - = 15 * 0.0005 * ceil(1 + 5/4) = 0.0225 crypto - open_value: (amount * open_rate) - (amount * open_rate * fee) - = (15 * 0.02) - (15 * 0.02 * 0.0025) - = 0.29925 - amount_closed: amount + interest = 15 + 0.009375 = 15.0225 - close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - = (15.0225 * 0.01) + (15.0225 * 0.01 * 0.0025) - = 0.15060056250000003 - total_profit = open_value - close_value - = 0.29925 - 0.15060056250000003 - = 0.14864943749999998 - total_profit_percentage = (1-(close_value/open_value)) * leverage - = (1 - (0.15060056250000003/0.29925)) * 3 - = 1.4902199248120298 - """ - trade = Trade( - pair='ETH/BTC', - stake_amount=0.1, - open_rate=0.02, - amount=15, - is_open=True, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_date=datetime.utcnow() - timedelta(hours=4, minutes=55), - exchange='kraken', - is_short=True, - leverage=3.0, - interest_rate=0.0005, - interest_mode=InterestMode.HOURSPER4 - ) - assert trade.close_profit is None - assert trade.close_date is None - assert trade.is_open is True - trade.close(0.01) - assert trade.is_open is False - assert trade.close_profit == round(1.4902199248120298, 8) - assert trade.close_date is not None - - # TODO-mg: Remove these comments probably - # new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, - # assert trade.close_date != new_date - # # Close should NOT update close_date if the trade has been closed already - # assert trade.is_open is False - # trade.close_date = new_date - # trade.close(0.02) - # assert trade.close_date == new_date - - -@ pytest.mark.usefixtures("init_persistence") -def test_update_with_binance_short(limit_short_order, limit_exit_short_order, fee, caplog): - """ - 10 minute short limit trade on binance - - Short trade - fee: 0.25% base - interest_rate: 0.05% per day - open_rate: 0.00001173 base - close_rate: 0.00001099 base - amount: 90.99181073 crypto - stake_amount: 0.0010673339398629 base - borrowed: 90.99181073 crypto - time-periods: 10 minutes(rounds up to 1/24 time-period of 1 day) - interest: borrowed * interest_rate * time-periods - = 90.99181073 * 0.0005 * 1/24 = 0.0018956627235416667 crypto - open_value: (amount * open_rate) - (amount * open_rate * fee) - = 90.99181073 * 0.00001173 - 90.99181073 * 0.00001173 * 0.0025 - = 0.0010646656050132426 - amount_closed: amount + interest = 90.99181073 + 0.0018956627235416667 = 90.99370639272354 - close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - = (90.99370639272354 * 0.00001099) + (90.99370639272354 * 0.00001099 * 0.0025) - = 0.0010025208853391716 - total_profit = open_value - close_value - = 0.0010646656050132426 - 0.0010025208853391716 - = 0.00006214471967407108 - total_profit_percentage = (1 - (close_value/open_value)) * leverage - = (1 - (0.0010025208853391716/0.0010646656050132426)) * 1 - = 0.05837017687191848 - - """ - trade = Trade( - id=2, - pair='ETH/BTC', - stake_amount=0.0010673339398629, - open_rate=0.01, - amount=5, - is_open=True, - open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), - fee_open=fee.return_value, - fee_close=fee.return_value, - # borrowed=90.99181073, - interest_rate=0.0005, - exchange='binance', - interest_mode=InterestMode.HOURSPERDAY - ) - # assert trade.open_order_id is None - assert trade.close_profit is None - assert trade.close_date is None - assert trade.borrowed == 0.0 - assert trade.is_short is None - # trade.open_order_id = 'something' - trade.update(limit_short_order) - # assert trade.open_order_id is None - assert trade.open_rate == 0.00001173 - assert trade.close_profit is None - assert trade.close_date is None - assert trade.borrowed == 90.99181073 - assert trade.is_short is True - assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, " - r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001173, open_since=.*\).", - caplog) - caplog.clear() - # trade.open_order_id = 'something' - trade.update(limit_exit_short_order) - # assert trade.open_order_id is None - assert trade.close_rate == 0.00001099 - assert trade.close_profit == round(0.05837017687191848, 8) - assert trade.close_date is not None - assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, " - r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001173, open_since=.*\).", - caplog) - - -@ pytest.mark.usefixtures("init_persistence") -def test_update_market_order_short( - market_short_order, - market_exit_short_order, - fee, - caplog -): - """ - 10 minute short market trade on Kraken at 3x leverage - Short trade - fee: 0.25% base - interest_rate: 0.05% per 4 hrs - open_rate: 0.00004173 base - close_rate: 0.00004099 base - amount: = 275.97543219 crypto - stake_amount: 0.0038388182617629 - borrowed: 275.97543219 crypto - time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) - interest: borrowed * interest_rate * time-periods - = 275.97543219 * 0.0005 * 2 = 0.27597543219 crypto - open_value: (amount * open_rate) - (amount * open_rate * fee) - = 275.97543219 * 0.00004173 - 275.97543219 * 0.00004173 * 0.0025 - = 0.011487663648325479 - amount_closed: amount + interest = 275.97543219 + 0.27597543219 = 276.25140762219 - close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - = (276.25140762219 * 0.00004099) + (276.25140762219 * 0.00004099 * 0.0025) - = 0.0034174647259 - total_profit = open_value - close_value - = 0.011487663648325479 - 0.0034174647259 - = 0.00013580958689582596 - total_profit_percentage = total_profit / stake_amount - = (1 - (close_value/open_value)) * leverage - = (1 - (0.0034174647259/0.011487663648325479)) * 3 - = 0.03546663387440563 - """ - trade = Trade( - id=1, - pair='ETH/BTC', - stake_amount=0.0038388182617629, - amount=5, - open_rate=0.01, - is_open=True, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), - interest_rate=0.0005, - exchange='kraken', - interest_mode=InterestMode.HOURSPER4 - ) - trade.open_order_id = 'something' - trade.update(market_short_order) - assert trade.leverage == 3.0 - assert trade.is_short is True - assert trade.open_order_id is None - assert trade.open_rate == 0.00004173 - assert trade.close_profit is None - assert trade.close_date is None - assert trade.interest_rate == 0.0005 - # The logger also has the exact same but there's some spacing in there - assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " - r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004173, open_since=.*\).", - caplog) - caplog.clear() - trade.is_open = True - trade.open_order_id = 'something' - trade.update(market_exit_short_order) - assert trade.open_order_id is None - assert trade.close_rate == 0.00004099 - assert trade.close_profit == round(0.03546663387440563, 8) - assert trade.close_date is not None - # TODO-mg: The amount should maybe be the opening amount + the interest - # TODO-mg: Uncomment the next assert and make it work. - # The logger also has the exact same but there's some spacing in there - assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, " - r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004173, open_since=.*\).", - caplog) - - -@ pytest.mark.usefixtures("init_persistence") -def test_calc_profit_short(market_short_order, market_exit_short_order, fee): - """ - Market trade on Kraken at 3x leverage - Short trade - fee: 0.25% base or 0.3% - interest_rate: 0.05%, 0.025% per 4 hrs - open_rate: 0.00004173 base - close_rate: 0.00004099 base - stake_amount: 0.0038388182617629 - amount: = 275.97543219 crypto - borrowed: 275.97543219 crypto - time-periods: 10 minutes(rounds up to 1 time-period of 4hrs) - 5 hours = 5/4 - - interest: borrowed * interest_rate * time-periods - = 275.97543219 * 0.0005 * ceil(1+1) = 0.27597543219 crypto - = 275.97543219 * 0.00025 * ceil(1+5/4) = 0.20698157414249999 crypto - = 275.97543219 * 0.0005 * ceil(1+5/4) = 0.41396314828499997 crypto - = 275.97543219 * 0.00025 * ceil(1+1) = 0.27597543219 crypto - = 275.97543219 * 0.00025 * ceil(1+1) = 0.27597543219 crypto - open_value: (amount * open_rate) - (amount * open_rate * fee) - = (275.97543219 * 0.00004173) - (275.97543219 * 0.00004173 * 0.0025) - = 0.011487663648325479 - amount_closed: amount + interest - = 275.97543219 + 0.27597543219 = 276.25140762219 - = 275.97543219 + 0.20698157414249999 = 276.1824137641425 - = 275.97543219 + 0.41396314828499997 = 276.389395338285 - = 275.97543219 + 0.27597543219 = 276.25140762219 - close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) - (276.25140762219 * 0.00004374) + (276.25140762219 * 0.00004374 * 0.0025) - = 0.012113444660818078 - (276.1824137641425 * 0.00000437) + (276.1824137641425 * 0.00000437 * 0.0025) - = 0.0012099344410196758 - (276.389395338285 * 0.00004374) + (276.389395338285 * 0.00004374 * 0.003) - = 0.012125539968552874 - (276.25140762219 * 0.00000437) + (276.25140762219 * 0.00000437 * 0.003) - = 0.0012102354919246037 - (276.25140762219 * 0.00004099) + (276.25140762219 * 0.00004099 * 0.0025) - = 0.011351854061429653 - total_profit = open_value - close_value - = 0.011487663648325479 - 0.012113444660818078 = -0.0006257810124925996 - = 0.011487663648325479 - 0.0012099344410196758 = 0.010277729207305804 - = 0.011487663648325479 - 0.012125539968552874 = -0.0006378763202273957 - = 0.011487663648325479 - 0.0012102354919246037 = 0.010277428156400875 - = 0.011487663648325479 - 0.011351854061429653 = 0.00013580958689582596 - total_profit_percentage = (1-(close_value/open_value)) * leverage - (1-(0.012113444660818078 /0.011487663648325479))*3 = -0.16342252828332549 - (1-(0.0012099344410196758/0.011487663648325479))*3 = 2.6840259748040123 - (1-(0.012125539968552874 /0.011487663648325479))*3 = -0.16658121435868578 - (1-(0.0012102354919246037/0.011487663648325479))*3 = 2.68394735544829 - (1-(0.011351854061429653/0.011487663648325479))*3 = 0.03546663387440563 - """ - trade = Trade( - pair='ETH/BTC', - stake_amount=0.0038388182617629, - amount=5, - open_rate=0.00001099, - open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='kraken', - is_short=True, - interest_rate=0.0005, - interest_mode=InterestMode.HOURSPER4 - ) - trade.open_order_id = 'something' - trade.update(market_short_order) # Buy @ 0.00001099 - # Custom closing rate and regular fee rate - - # Higher than open rate - assert trade.calc_profit( - rate=0.00004374, interest_rate=0.0005) == round(-0.0006257810124925996, 8) - assert trade.calc_profit_ratio( - rate=0.00004374, interest_rate=0.0005) == round(-0.16342252828332549, 8) - - # Lower than open rate - trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55) - assert trade.calc_profit(rate=0.00000437, interest_rate=0.00025) == round( - 0.010277729207305804, 8) - assert trade.calc_profit_ratio( - rate=0.00000437, interest_rate=0.00025) == round(2.6840259748040123, 8) - - # Custom closing rate and custom fee rate - # Higher than open rate - assert trade.calc_profit(rate=0.00004374, fee=0.003, - interest_rate=0.0005) == round(-0.0006378763202273957, 8) - assert trade.calc_profit_ratio(rate=0.00004374, fee=0.003, - interest_rate=0.0005) == round(-0.16658121435868578, 8) - - # Lower than open rate - trade.open_date = datetime.utcnow() - timedelta(hours=0, minutes=10) - assert trade.calc_profit(rate=0.00000437, fee=0.003, - interest_rate=0.00025) == round(0.010277428156400875, 8) - assert trade.calc_profit_ratio(rate=0.00000437, fee=0.003, - interest_rate=0.00025) == round(2.68394735544829, 8) - - # Test when we apply a exit short order. - trade.update(market_exit_short_order) - assert trade.calc_profit(rate=0.00004099) == round(0.00013580958689582596, 8) - assert trade.calc_profit_ratio() == round(0.03546663387440563, 8) - - # Test with a custom fee rate on the close trade - # assert trade.calc_profit(fee=0.003) == 0.00006163 - # assert trade.calc_profit_ratio(fee=0.003) == 0.06147824 - - -def test_adjust_stop_loss_short(fee): - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=5, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', - open_rate=1, - max_rate=1, - is_short=True, - interest_mode=InterestMode.HOURSPERDAY - ) - trade.adjust_stop_loss(trade.open_rate, 0.05, True) - assert trade.stop_loss == 1.05 - assert trade.stop_loss_pct == 0.05 - assert trade.initial_stop_loss == 1.05 - assert trade.initial_stop_loss_pct == 0.05 - # Get percent of profit with a lower rate - trade.adjust_stop_loss(1.04, 0.05) - assert trade.stop_loss == 1.05 - assert trade.stop_loss_pct == 0.05 - assert trade.initial_stop_loss == 1.05 - assert trade.initial_stop_loss_pct == 0.05 - # Get percent of profit with a custom rate (Higher than open rate) - trade.adjust_stop_loss(0.7, 0.1) - # If the price goes down to 0.7, with a trailing stop of 0.1, - # the new stoploss at 0.1 above 0.7 would be 0.7*0.1 higher - assert round(trade.stop_loss, 8) == 0.77 - assert trade.stop_loss_pct == 0.1 - assert trade.initial_stop_loss == 1.05 - assert trade.initial_stop_loss_pct == 0.05 - # current rate lower again ... should not change - trade.adjust_stop_loss(0.8, -0.1) - assert round(trade.stop_loss, 8) == 0.77 - assert trade.initial_stop_loss == 1.05 - assert trade.initial_stop_loss_pct == 0.05 - # current rate higher... should raise stoploss - trade.adjust_stop_loss(0.6, -0.1) - assert round(trade.stop_loss, 8) == 0.66 - assert trade.initial_stop_loss == 1.05 - assert trade.initial_stop_loss_pct == 0.05 - # Initial is true but stop_loss set - so doesn't do anything - trade.adjust_stop_loss(0.3, -0.1, True) - assert round(trade.stop_loss, 8) == 0.66 # TODO-mg: What is this test? - assert trade.initial_stop_loss == 1.05 - assert trade.initial_stop_loss_pct == 0.05 - assert trade.stop_loss_pct == 0.1 - trade.set_liquidation_price(0.63) - trade.adjust_stop_loss(0.59, -0.1) - assert trade.stop_loss == 0.63 - assert trade.liquidation_price == 0.63 - - # TODO-mg: Do a test with a trade that has a liquidation price - - -@ pytest.mark.usefixtures("init_persistence") -@ pytest.mark.parametrize('use_db', [True, False]) -def test_get_open_short(fee, use_db): - Trade.use_db = use_db - Trade.reset_trades() - create_mock_trades_with_leverage(fee, use_db) - assert len(Trade.get_open_trades()) == 5 - Trade.use_db = True - - -def test_stoploss_reinitialization_short(default_conf, fee): - init_db(default_conf['db_url']) - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - fee_open=fee.return_value, - open_date=arrow.utcnow().shift(hours=-2).datetime, - amount=10, - fee_close=fee.return_value, - exchange='binance', - open_rate=1, - max_rate=1, - is_short=True, - leverage=3.0, - interest_mode=InterestMode.HOURSPERDAY - ) - trade.adjust_stop_loss(trade.open_rate, -0.05, True) - assert trade.stop_loss == 1.05 - assert trade.stop_loss_pct == 0.05 - assert trade.initial_stop_loss == 1.05 - assert trade.initial_stop_loss_pct == 0.05 - Trade.query.session.add(trade) - # Lower stoploss - Trade.stoploss_reinitialization(-0.06) - trades = Trade.get_open_trades() - assert len(trades) == 1 - trade_adj = trades[0] - assert trade_adj.stop_loss == 1.06 - assert trade_adj.stop_loss_pct == 0.06 - assert trade_adj.initial_stop_loss == 1.06 - assert trade_adj.initial_stop_loss_pct == 0.06 - # Raise stoploss - Trade.stoploss_reinitialization(-0.04) - trades = Trade.get_open_trades() - assert len(trades) == 1 - trade_adj = trades[0] - assert trade_adj.stop_loss == 1.04 - assert trade_adj.stop_loss_pct == 0.04 - assert trade_adj.initial_stop_loss == 1.04 - assert trade_adj.initial_stop_loss_pct == 0.04 - # Trailing stoploss - trade.adjust_stop_loss(0.98, -0.04) - assert trade_adj.stop_loss == 1.0192 - assert trade_adj.initial_stop_loss == 1.04 - Trade.stoploss_reinitialization(-0.04) - trades = Trade.get_open_trades() - assert len(trades) == 1 - trade_adj = trades[0] - # Stoploss should not change in this case. - assert trade_adj.stop_loss == 1.0192 - assert trade_adj.stop_loss_pct == 0.04 - assert trade_adj.initial_stop_loss == 1.04 - assert trade_adj.initial_stop_loss_pct == 0.04 - # Stoploss can't go above liquidation price - trade_adj.set_liquidation_price(1.0) - trade.adjust_stop_loss(0.97, -0.04) - assert trade_adj.stop_loss == 1.0 - assert trade_adj.stop_loss == 1.0 - - -@ pytest.mark.usefixtures("init_persistence") -@ pytest.mark.parametrize('use_db', [True, False]) -def test_total_open_trades_stakes_short(fee, use_db): - Trade.use_db = use_db - Trade.reset_trades() - res = Trade.total_open_trades_stakes() - assert res == 0 - create_mock_trades_with_leverage(fee, use_db) - res = Trade.total_open_trades_stakes() - assert res == 15.133 - Trade.use_db = True - - -@ pytest.mark.usefixtures("init_persistence") -def test_get_best_pair_short(fee): - res = Trade.get_best_pair() - assert res is None - create_mock_trades_with_leverage(fee) - res = Trade.get_best_pair() - assert len(res) == 2 - assert res[0] == 'DOGE/BTC' - assert res[1] == 0.1713156134055116 diff --git a/tests/persistence/test_persistence.py b/tests/test_persistence.py similarity index 51% rename from tests/persistence/test_persistence.py rename to tests/test_persistence.py index 913a40ca1..7c9df6258 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/test_persistence.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring, C0103 import logging from datetime import datetime, timedelta, timezone +from math import isclose from pathlib import Path from types import FunctionType from unittest.mock import MagicMock @@ -10,9 +11,10 @@ import pytest from sqlalchemy import create_engine, inspect, text from freqtrade import constants +from freqtrade.enums import InterestMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db -from tests.conftest import create_mock_trades, log_has, log_has_re +from tests.conftest import create_mock_trades, create_mock_trades_with_leverage, log_has, log_has_re def test_init_create_session(default_conf): @@ -158,7 +160,7 @@ def test_set_stop_loss_liquidation_price(fee): assert trade.stop_loss == 0.07 assert trade.initial_stop_loss == 0.07 - trade.is_short = True + trade.set_is_short(True) trade.stop_loss = None trade.initial_stop_loss = None @@ -189,40 +191,318 @@ def test_set_stop_loss_liquidation_price(fee): @pytest.mark.usefixtures("init_persistence") -def test_update_with_binance(limit_buy_order, limit_sell_order, fee, caplog): +def test_interest(market_buy_order_usdt, fee): + """ + 10min, 5hr limit trade on Binance/Kraken at 3x,5x leverage + fee: 0.25 % quote + interest_rate: 0.05 % per 4 hrs + open_rate: 2.00 quote + close_rate: 2.20 quote + amount: = 30.0 crypto + stake_amount + 3x, -3x: 20.0 quote + 5x, -5x: 12.0 quote + borrowed + 10min + 3x: 40 quote + -3x: 30 crypto + 5x: 48 quote + -5x: 30 crypto + 1x: 0 + -1x: 30 crypto + hours: 1/6 (10 minutes) + time-periods: + 10min + kraken: (1 + 1) 4hr_periods = 2 4hr_periods + binance: 1/24 24hr_periods + 4.95hr + kraken: ceil(1 + 4.95/4) 4hr_periods = 3 4hr_periods + binance: ceil(4.95)/24 24hr_periods = 5/24 24hr_periods + interest: borrowed * interest_rate * time-periods + 10min + binance 3x: 40 * 0.0005 * 1/24 = 0.0008333333333333334 quote + kraken 3x: 40 * 0.0005 * 2 = 0.040 quote + binace -3x: 30 * 0.0005 * 1/24 = 0.000625 crypto + kraken -3x: 30 * 0.0005 * 2 = 0.030 crypto + 5hr + binance 3x: 40 * 0.0005 * 5/24 = 0.004166666666666667 quote + kraken 3x: 40 * 0.0005 * 3 = 0.06 quote + binace -3x: 30 * 0.0005 * 5/24 = 0.0031249999999999997 crypto + kraken -3x: 30 * 0.0005 * 3 = 0.045 crypto + 0.00025 interest + binance 3x: 40 * 0.00025 * 5/24 = 0.0020833333333333333 quote + kraken 3x: 40 * 0.00025 * 3 = 0.03 quote + binace -3x: 30 * 0.00025 * 5/24 = 0.0015624999999999999 crypto + kraken -3x: 30 * 0.00025 * 3 = 0.0225 crypto + 5x leverage, 0.0005 interest, 5hr + binance 5x: 48 * 0.0005 * 5/24 = 0.005 quote + kraken 5x: 48 * 0.0005 * 3 = 0.07200000000000001 quote + binace -5x: 30 * 0.0005 * 5/24 = 0.0031249999999999997 crypto + kraken -5x: 30 * 0.0005 * 3 = 0.045 crypto + 1x leverage, 0.0005 interest, 5hr + binance,kraken 1x: 0.0 quote + binace -1x: 30 * 0.0005 * 5/24 = 0.003125 crypto + kraken -1x: 30 * 0.0005 * 3 = 0.045 crypto """ - On this test we will buy and sell a crypto currency. - Buy - - Buy: 90.99181073 Crypto at 0.00001099 BTC - (90.99181073*0.00001099 = 0.0009999 BTC) - - Buying fee: 0.25% - - Total cost of buy trade: 0.001002500 BTC - ((90.99181073*0.00001099) + ((90.99181073*0.00001099)*0.0025)) + trade = Trade( + pair='ETH/BTC', + stake_amount=20.0, + amount=30.0, + open_rate=2.0, + open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='kraken', + leverage=3.0, + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY + ) - Sell - - Sell: 90.99181073 Crypto at 0.00001173 BTC - (90.99181073*0.00001173 = 0,00106733394 BTC) - - Selling fee: 0.25% - - Total cost of sell trade: 0.001064666 BTC - ((90.99181073*0.00001173) - ((90.99181073*0.00001173)*0.0025)) + # 10min, 3x leverage + # binance + assert round(float(trade.calculate_interest()), 8) == round(0.0008333333333333334, 8) + # kraken + trade.interest_mode = InterestMode.HOURSPER4 + assert float(trade.calculate_interest()) == 0.040 + # Short + trade.set_is_short(True) + # binace + trade.interest_mode = InterestMode.HOURSPERDAY + assert float(trade.calculate_interest()) == 0.000625 + # kraken + trade.interest_mode = InterestMode.HOURSPER4 + assert isclose(float(trade.calculate_interest()), 0.030) - Profit/Loss: +0.000062166 BTC - (Sell:0.001064666 - Buy:0.001002500) - Profit/Loss percentage: 0.0620 - ((0.001064666/0.001002500)-1 = 6.20%) + # 5hr, long + trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55) + trade.set_is_short(False) + # binance + trade.interest_mode = InterestMode.HOURSPERDAY + assert round(float(trade.calculate_interest()), 8) == round(0.004166666666666667, 8) + # kraken + trade.interest_mode = InterestMode.HOURSPER4 + assert float(trade.calculate_interest()) == 0.06 + # short + trade.set_is_short(True) + # binace + trade.interest_mode = InterestMode.HOURSPERDAY + assert round(float(trade.calculate_interest()), 8) == round(0.0031249999999999997, 8) + # kraken + trade.interest_mode = InterestMode.HOURSPER4 + assert float(trade.calculate_interest()) == 0.045 - :param limit_buy_order: - :param limit_sell_order: - :return: + # 0.00025 interest, 5hr, long + trade.set_is_short(False) + # binance + trade.interest_mode = InterestMode.HOURSPERDAY + assert round(float(trade.calculate_interest(interest_rate=0.00025)), + 8) == round(0.0020833333333333333, 8) + # kraken + trade.interest_mode = InterestMode.HOURSPER4 + assert isclose(float(trade.calculate_interest(interest_rate=0.00025)), 0.03) + # short + trade.set_is_short(True) + # binace + trade.interest_mode = InterestMode.HOURSPERDAY + assert round(float(trade.calculate_interest(interest_rate=0.00025)), + 8) == round(0.0015624999999999999, 8) + # kraken + trade.interest_mode = InterestMode.HOURSPER4 + assert float(trade.calculate_interest(interest_rate=0.00025)) == 0.0225 + + # 5x leverage, 0.0005 interest, 5hr, long + trade.set_is_short(False) + trade.leverage = 5.0 + # binance + trade.interest_mode = InterestMode.HOURSPERDAY + assert round(float(trade.calculate_interest()), 8) == 0.005 + # kraken + trade.interest_mode = InterestMode.HOURSPER4 + assert float(trade.calculate_interest()) == round(0.07200000000000001, 8) + # short + trade.set_is_short(True) + # binace + trade.interest_mode = InterestMode.HOURSPERDAY + assert round(float(trade.calculate_interest()), 8) == round(0.0031249999999999997, 8) + # kraken + trade.interest_mode = InterestMode.HOURSPER4 + assert float(trade.calculate_interest()) == 0.045 + + # 1x leverage, 0.0005 interest, 5hr + trade.set_is_short(False) + trade.leverage = 1.0 + # binance + trade.interest_mode = InterestMode.HOURSPERDAY + assert float(trade.calculate_interest()) == 0.0 + # kraken + trade.interest_mode = InterestMode.HOURSPER4 + assert float(trade.calculate_interest()) == 0.0 + # short + trade.set_is_short(True) + # binace + trade.interest_mode = InterestMode.HOURSPERDAY + assert float(trade.calculate_interest()) == 0.003125 + # kraken + trade.interest_mode = InterestMode.HOURSPER4 + assert float(trade.calculate_interest()) == 0.045 + + +@pytest.mark.usefixtures("init_persistence") +def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee, caplog): + """ + 10 minute limit trade on Binance/Kraken at 1x, 3x leverage + fee: 0.25% quote + interest_rate: 0.05% per 4 hrs + open_rate: 2.00 quote + close_rate: 2.20 quote + amount: = 30.0 crypto + stake_amount + 1x,-1x: 60.0 quote + 3x,-3x: 20.0 quote + borrowed + 1x: 0 quote + 3x: 40 quote + -1x: 30 crypto + -3x: 30 crypto + hours: 1/6 (10 minutes) + time-periods: + kraken: (1 + 1) 4hr_periods = 2 4hr_periods + binance: 1/24 24hr_periods + interest: borrowed * interest_rate * time-periods + 1x : / + binance 3x: 40 * 0.0005 * 1/24 = 0.0008333333333333334 quote + kraken 3x: 40 * 0.0005 * 2 = 0.040 quote + binace -1x,-3x: 30 * 0.0005 * 1/24 = 0.000625 crypto + kraken -1x,-3x: 30 * 0.0005 * 2 = 0.030 crypto + open_value: (amount * open_rate) ± (amount * open_rate * fee) + 1x, 3x: 30 * 2 + 30 * 2 * 0.0025 = 60.15 quote + -1x,-3x: 30 * 2 - 30 * 2 * 0.0025 = 59.850 quote + amount_closed: + 1x, 3x : amount + -1x, -3x : amount + interest + binance -1x,-3x: 30 + 0.000625 = 30.000625 crypto + kraken -1x,-3x: 30 + 0.03 = 30.03 crypto + close_value: + 1x, 3x: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest + -1x,-3x: (amount_closed * close_rate) + (amount_closed * close_rate * fee) + binance,kraken 1x: (30.00 * 2.20) - (30.00 * 2.20 * 0.0025) = 65.835 + binance 3x: (30.00 * 2.20) - (30.00 * 2.20 * 0.0025) - 0.00083333 = 65.83416667 + kraken 3x: (30.00 * 2.20) - (30.00 * 2.20 * 0.0025) - 0.040 = 65.795 + binance -1x,-3x: (30.000625 * 2.20) + (30.000625 * 2.20 * 0.0025) = 66.16637843750001 + kraken -1x,-3x: (30.03 * 2.20) + (30.03 * 2.20 * 0.0025) = 66.231165 + total_profit: + 1x, 3x : close_value - open_value + -1x,-3x: open_value - close_value + binance,kraken 1x: 65.835 - 60.15 = 5.685 + binance 3x: 65.83416667 - 60.15 = 5.684166670000003 + kraken 3x: 65.795 - 60.15 = 5.645 + binance -1x,-3x: 59.850 - 66.16637843750001 = -6.316378437500013 + kraken -1x,-3x: 59.850 - 66.231165 = -6.381165 + total_profit_ratio: + 1x, 3x : ((close_value/open_value) - 1) * leverage + -1x,-3x: (1 - (close_value/open_value)) * leverage + binance 1x: ((65.835 / 60.15) - 1) * 1 = 0.0945137157107232 + binance 3x: ((65.83416667 / 60.15) - 1) * 3 = 0.2834995845386534 + kraken 1x: ((65.835 / 60.15) - 1) * 1 = 0.0945137157107232 + kraken 3x: ((65.795 / 60.15) - 1) * 3 = 0.2815461346633419 + binance -1x: (1-(66.1663784375 / 59.85)) * 1 = -0.1055368159983292 + binance -3x: (1-(66.1663784375 / 59.85)) * 3 = -0.3166104479949876 + kraken -1x: (1-(66.2311650 / 59.85)) * 1 = -0.106619298245614 + kraken -3x: (1-(66.2311650 / 59.85)) * 3 = -0.319857894736842 """ trade = Trade( id=2, pair='ETH/BTC', - stake_amount=0.001, - open_rate=0.01, - amount=5, + stake_amount=60.0, + open_rate=2.0, + amount=30.0, + is_open=True, + open_date=arrow.utcnow().datetime, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + ) + assert trade.borrowed == 0 + trade.set_is_short(True) + assert trade.borrowed == 30.0 + trade.leverage = 3.0 + assert trade.borrowed == 30.0 + trade.set_is_short(False) + assert trade.borrowed == 40.0 + + +@pytest.mark.usefixtures("init_persistence") +def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, caplog): + """ + 10 minute limit trade on Binance/Kraken at 1x, 3x leverage + fee: 0.25% quote + interest_rate: 0.05% per 4 hrs + open_rate: 2.00 quote + close_rate: 2.20 quote + amount: = 30.0 crypto + stake_amount + 1x,-1x: 60.0 quote + 3x,-3x: 20.0 quote + borrowed + 1x: 0 quote + 3x: 40 quote + -1x: 30 crypto + -3x: 30 crypto + hours: 1/6 (10 minutes) + time-periods: + kraken: (1 + 1) 4hr_periods = 2 4hr_periods + binance: 1/24 24hr_periods + interest: borrowed * interest_rate * time-periods + 1x : / + binance 3x: 40 * 0.0005 * 1/24 = 0.0008333333333333334 quote + kraken 3x: 40 * 0.0005 * 2 = 0.040 quote + binace -1x,-3x: 30 * 0.0005 * 1/24 = 0.000625 crypto + kraken -1x,-3x: 30 * 0.0005 * 2 = 0.030 crypto + open_value: (amount * open_rate) ± (amount * open_rate * fee) + 1x, 3x: 30 * 2 + 30 * 2 * 0.0025 = 60.15 quote + -1x,-3x: 30 * 2 - 30 * 2 * 0.0025 = 59.850 quote + amount_closed: + 1x, 3x : amount + -1x, -3x : amount + interest + binance -1x,-3x: 30 + 0.000625 = 30.000625 crypto + kraken -1x,-3x: 30 + 0.03 = 30.03 crypto + close_value: + 1x, 3x: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest + -1x,-3x: (amount_closed * close_rate) + (amount_closed * close_rate * fee) + binance,kraken 1x: (30.00 * 2.20) - (30.00 * 2.20 * 0.0025) = 65.835 + binance 3x: (30.00 * 2.20) - (30.00 * 2.20 * 0.0025) - 0.00083333 = 65.83416667 + kraken 3x: (30.00 * 2.20) - (30.00 * 2.20 * 0.0025) - 0.040 = 65.795 + binance -1x,-3x: (30.000625 * 2.20) + (30.000625 * 2.20 * 0.0025) = 66.16637843750001 + kraken -1x,-3x: (30.03 * 2.20) + (30.03 * 2.20 * 0.0025) = 66.231165 + total_profit: + 1x, 3x : close_value - open_value + -1x,-3x: open_value - close_value + binance,kraken 1x: 65.835 - 60.15 = 5.685 + binance 3x: 65.83416667 - 60.15 = 5.684166670000003 + kraken 3x: 65.795 - 60.15 = 5.645 + binance -1x,-3x: 59.850 - 66.16637843750001 = -6.316378437500013 + kraken -1x,-3x: 59.850 - 66.231165 = -6.381165 + total_profit_ratio: + 1x, 3x : ((close_value/open_value) - 1) * leverage + -1x,-3x: (1 - (close_value/open_value)) * leverage + binance 1x: ((65.835 / 60.15) - 1) * 1 = 0.0945137157107232 + binance 3x: ((65.83416667 / 60.15) - 1) * 3 = 0.2834995845386534 + kraken 1x: ((65.835 / 60.15) - 1) * 1 = 0.0945137157107232 + kraken 3x: ((65.795 / 60.15) - 1) * 3 = 0.2815461346633419 + binance -1x: (1-(66.1663784375 / 59.85)) * 1 = -0.1055368159983292 + binance -3x: (1-(66.1663784375 / 59.85)) * 3 = -0.3166104479949876 + kraken -1x: (1-(66.2311650 / 59.85)) * 1 = -0.106619298245614 + kraken -3x: (1-(66.2311650 / 59.85)) * 3 = -0.319857894736842 + """ + + trade = Trade( + id=2, + pair='ETH/BTC', + stake_amount=60.0, + open_rate=2.0, + amount=30.0, is_open=True, open_date=arrow.utcnow().datetime, fee_open=fee.return_value, @@ -234,35 +514,35 @@ def test_update_with_binance(limit_buy_order, limit_sell_order, fee, caplog): assert trade.close_date is None trade.open_order_id = 'something' - trade.update(limit_buy_order) + trade.update(limit_buy_order_usdt) assert trade.open_order_id is None - assert trade.open_rate == 0.00001099 + assert trade.open_rate == 2.00 assert trade.close_profit is None assert trade.close_date is None assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, " - r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=.*\).", + r"pair=ETH/BTC, amount=30.00000000, open_rate=2.00000000, open_since=.*\).", caplog) caplog.clear() trade.open_order_id = 'something' - trade.update(limit_sell_order) + trade.update(limit_sell_order_usdt) assert trade.open_order_id is None - assert trade.close_rate == 0.00001173 - assert trade.close_profit == 0.06201058 + assert trade.close_rate == 2.20 + assert trade.close_profit == round(0.0945137157107232, 8) assert trade.close_date is not None assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, " - r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=.*\).", + r"pair=ETH/BTC, amount=30.00000000, open_rate=2.00000000, open_since=.*\).", caplog) @pytest.mark.usefixtures("init_persistence") -def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): +def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, caplog): trade = Trade( id=1, pair='ETH/BTC', - stake_amount=0.001, - amount=5, - open_rate=0.01, + stake_amount=60.0, + open_rate=2.0, + amount=30.0, is_open=True, fee_open=fee.return_value, fee_close=fee.return_value, @@ -271,73 +551,111 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): ) trade.open_order_id = 'something' - trade.update(market_buy_order) + trade.update(market_buy_order_usdt) assert trade.open_order_id is None - assert trade.open_rate == 0.00004099 + assert trade.open_rate == 2.0 assert trade.close_profit is None assert trade.close_date is None assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, " - r"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=.*\).", + r"pair=ETH/BTC, amount=30.00000000, open_rate=2.00000000, open_since=.*\).", caplog) caplog.clear() trade.is_open = True trade.open_order_id = 'something' - trade.update(market_sell_order) + trade.update(market_sell_order_usdt) assert trade.open_order_id is None - assert trade.close_rate == 0.00004173 - assert trade.close_profit == 0.01297561 + assert trade.close_rate == 2.2 + assert trade.close_profit == round(0.0945137157107232, 8) assert trade.close_date is not None assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " - r"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=.*\).", + r"pair=ETH/BTC, amount=30.00000000, open_rate=2.00000000, open_since=.*\).", caplog) @pytest.mark.usefixtures("init_persistence") -def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): +def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee): trade = Trade( pair='ETH/BTC', - stake_amount=0.001, - open_rate=0.01, - amount=5, + stake_amount=60.0, + open_rate=2.0, + amount=30.0, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10), + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY, fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', ) trade.open_order_id = 'something' - trade.update(limit_buy_order) - assert trade._calc_open_trade_value() == 0.0010024999999225068 - - trade.update(limit_sell_order) - assert trade.calc_close_trade_value() == 0.0010646656050132426 - - # Profit in BTC - assert trade.calc_profit() == 0.00006217 - - # Profit in percent - assert trade.calc_profit_ratio() == 0.06201058 + trade.update(limit_buy_order_usdt) + trade.update(limit_sell_order_usdt) + # 1x leverage, binance + assert trade._calc_open_trade_value() == 60.15 + assert isclose(trade.calc_close_trade_value(), 65.835) + assert trade.calc_profit() == 5.685 + assert trade.calc_profit_ratio() == round(0.0945137157107232, 8) + # 3x leverage, binance + trade.leverage = 3 + trade.interest_mode = InterestMode.HOURSPERDAY + assert trade._calc_open_trade_value() == 60.15 + assert round(trade.calc_close_trade_value(), 8) == 65.83416667 + assert trade.calc_profit() == round(5.684166670000003, 8) + assert trade.calc_profit_ratio() == round(0.2834995845386534, 8) + trade.interest_mode = InterestMode.HOURSPER4 + # 3x leverage, kraken + assert trade._calc_open_trade_value() == 60.15 + assert trade.calc_close_trade_value() == 65.795 + assert trade.calc_profit() == 5.645 + assert trade.calc_profit_ratio() == round(0.2815461346633419, 8) + trade.set_is_short(True) + # 3x leverage, short, kraken + assert trade._calc_open_trade_value() == 59.850 + assert trade.calc_close_trade_value() == 66.231165 + assert trade.calc_profit() == round(-6.381165000000003, 8) + assert trade.calc_profit_ratio() == round(-0.319857894736842, 8) + trade.interest_mode = InterestMode.HOURSPERDAY + # 3x leverage, short, binance + assert trade._calc_open_trade_value() == 59.85 + assert trade.calc_close_trade_value() == 66.1663784375 + assert trade.calc_profit() == round(-6.316378437500013, 8) + assert trade.calc_profit_ratio() == round(-0.3166104479949876, 8) + # 1x leverage, short, binance + trade.leverage = 1.0 + assert trade._calc_open_trade_value() == 59.850 + assert trade.calc_close_trade_value() == 66.1663784375 + assert trade.calc_profit() == round(-6.316378437500013, 8) + assert trade.calc_profit_ratio() == round(-0.1055368159983292, 8) + # 1x leverage, short, kraken + trade.interest_mode = InterestMode.HOURSPER4 + assert trade._calc_open_trade_value() == 59.850 + assert trade.calc_close_trade_value() == 66.231165 + assert trade.calc_profit() == -6.381165 + assert trade.calc_profit_ratio() == round(-0.106619298245614, 8) @pytest.mark.usefixtures("init_persistence") -def test_trade_close(limit_buy_order, limit_sell_order, fee): +def test_trade_close(limit_buy_order_usdt, limit_sell_order_usdt, fee): trade = Trade( pair='ETH/BTC', - stake_amount=0.001, - open_rate=0.01, - amount=5, + stake_amount=60.0, + open_rate=2.0, + amount=30.0, is_open=True, fee_open=fee.return_value, fee_close=fee.return_value, - open_date=arrow.Arrow(2020, 2, 1, 15, 5, 1).datetime, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10), + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY, exchange='binance', ) assert trade.close_profit is None assert trade.close_date is None assert trade.is_open is True - trade.close(0.02) + trade.close(2.2) assert trade.is_open is False - assert trade.close_profit == 0.99002494 + assert trade.close_profit == round(0.0945137157107232, 8) assert trade.close_date is not None new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, @@ -350,29 +668,29 @@ def test_trade_close(limit_buy_order, limit_sell_order, fee): @pytest.mark.usefixtures("init_persistence") -def test_calc_close_trade_price_exception(limit_buy_order, fee): +def test_calc_close_trade_price_exception(limit_buy_order_usdt, fee): trade = Trade( pair='ETH/BTC', - stake_amount=0.001, - open_rate=0.1, - amount=5, + stake_amount=60.0, + open_rate=2.0, + amount=30.0, fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', ) trade.open_order_id = 'something' - trade.update(limit_buy_order) + trade.update(limit_buy_order_usdt) assert trade.calc_close_trade_value() == 0.0 @pytest.mark.usefixtures("init_persistence") -def test_update_open_order(limit_buy_order): +def test_update_open_order(limit_buy_order_usdt): trade = Trade( pair='ETH/BTC', - stake_amount=1.00, - open_rate=0.01, - amount=5, + stake_amount=60.0, + open_rate=2.0, + amount=30.0, fee_open=0.1, fee_close=0.1, exchange='binance', @@ -382,8 +700,8 @@ def test_update_open_order(limit_buy_order): assert trade.close_profit is None assert trade.close_date is None - limit_buy_order['status'] = 'open' - trade.update(limit_buy_order) + limit_buy_order_usdt['status'] = 'open' + trade.update(limit_buy_order_usdt) assert trade.open_order_id is None assert trade.close_profit is None @@ -391,130 +709,451 @@ def test_update_open_order(limit_buy_order): @pytest.mark.usefixtures("init_persistence") -def test_update_invalid_order(limit_buy_order): +def test_update_invalid_order(limit_buy_order_usdt): trade = Trade( pair='ETH/BTC', - stake_amount=1.00, - amount=5, - open_rate=0.001, + stake_amount=60.0, + amount=30.0, + open_rate=2.0, fee_open=0.1, fee_close=0.1, exchange='binance', ) - limit_buy_order['type'] = 'invalid' + limit_buy_order_usdt['type'] = 'invalid' with pytest.raises(ValueError, match=r'Unknown order type'): - trade.update(limit_buy_order) + trade.update(limit_buy_order_usdt) @pytest.mark.usefixtures("init_persistence") -def test_calc_open_trade_value(limit_buy_order, fee): +def test_calc_open_trade_value(limit_buy_order_usdt, fee): + # 10 minute limit trade on Binance/Kraken at 1x, 3x leverage + # fee: 0.25 %, 0.3% quote + # open_rate: 2.00 quote + # amount: = 30.0 crypto + # stake_amount + # 1x, -1x: 60.0 quote + # 3x, -3x: 20.0 quote + # open_value: (amount * open_rate) ± (amount * open_rate * fee) + # 0.25% fee + # 1x, 3x: 30 * 2 + 30 * 2 * 0.0025 = 60.15 quote + # -1x,-3x: 30 * 2 - 30 * 2 * 0.0025 = 59.85 quote + # 0.3% fee + # 1x, 3x: 30 * 2 + 30 * 2 * 0.003 = 60.18 quote + # -1x,-3x: 30 * 2 - 30 * 2 * 0.003 = 59.82 quote trade = Trade( pair='ETH/BTC', - stake_amount=0.001, - amount=5, - open_rate=0.00001099, + stake_amount=60.0, + amount=30.0, + open_rate=2.0, fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', ) trade.open_order_id = 'open_trade' - trade.update(limit_buy_order) # Buy @ 0.00001099 + trade.update(limit_buy_order_usdt) # Get the open rate price with the standard fee rate - assert trade._calc_open_trade_value() == 0.0010024999999225068 - trade.fee_open = 0.003 + assert trade._calc_open_trade_value() == 60.15 + trade.set_is_short(True) + assert trade._calc_open_trade_value() == 59.85 + trade.leverage = 3 + trade.interest_mode = InterestMode.HOURSPERDAY + assert trade._calc_open_trade_value() == 59.85 + trade.set_is_short(False) + assert trade._calc_open_trade_value() == 60.15 + # Get the open rate price with a custom fee rate - assert trade._calc_open_trade_value() == 0.001002999999922468 + trade.fee_open = 0.003 + + assert trade._calc_open_trade_value() == 60.18 + trade.set_is_short(True) + assert trade._calc_open_trade_value() == 59.82 @pytest.mark.usefixtures("init_persistence") -def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee): +def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee): trade = Trade( pair='ETH/BTC', - stake_amount=0.001, - amount=5, - open_rate=0.00001099, + stake_amount=60.0, + amount=30.0, + open_rate=2.0, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10), fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY ) trade.open_order_id = 'close_trade' - trade.update(limit_buy_order) # Buy @ 0.00001099 + trade.update(limit_buy_order_usdt) - # Get the close rate price with a custom close rate and a regular fee rate - assert trade.calc_close_trade_value(rate=0.00001234) == 0.0011200318470471794 + # 1x leverage binance + assert trade.calc_close_trade_value(rate=2.5) == 74.8125 + assert trade.calc_close_trade_value(rate=2.5, fee=0.003) == 74.775 + trade.update(limit_sell_order_usdt) + assert trade.calc_close_trade_value(fee=0.005) == 65.67 - # Get the close rate price with a custom close rate and a custom fee rate - assert trade.calc_close_trade_value(rate=0.00001234, fee=0.003) == 0.0011194704275749754 + # 3x leverage binance + trade.leverage = 3.0 + assert round(trade.calc_close_trade_value(rate=2.5), 8) == 74.81166667 + assert round(trade.calc_close_trade_value(rate=2.5, fee=0.003), 8) == 74.77416667 - # Test when we apply a Sell order, and ask price with a custom fee rate - trade.update(limit_sell_order) - assert trade.calc_close_trade_value(fee=0.005) == 0.0010619972701635854 + # 3x leverage kraken + trade.interest_mode = InterestMode.HOURSPER4 + assert trade.calc_close_trade_value(rate=2.5) == 74.7725 + assert trade.calc_close_trade_value(rate=2.5, fee=0.003) == 74.735 + + # 3x leverage kraken, short + trade.set_is_short(True) + assert round(trade.calc_close_trade_value(rate=2.5), 8) == 75.2626875 + assert trade.calc_close_trade_value(rate=2.5, fee=0.003) == 75.300225 + + # 3x leverage binance, short + trade.interest_mode = InterestMode.HOURSPERDAY + assert round(trade.calc_close_trade_value(rate=2.5), 8) == 75.18906641 + assert round(trade.calc_close_trade_value(rate=2.5, fee=0.003), 8) == 75.22656719 + + trade.leverage = 1.0 + # 1x leverage binance, short + assert round(trade.calc_close_trade_value(rate=2.5), 8) == 75.18906641 + assert round(trade.calc_close_trade_value(rate=2.5, fee=0.003), 8) == 75.22656719 + + # 1x leverage kraken, short + trade.interest_mode = InterestMode.HOURSPER4 + assert round(trade.calc_close_trade_value(rate=2.5), 8) == 75.2626875 + assert trade.calc_close_trade_value(rate=2.5, fee=0.003) == 75.300225 @pytest.mark.usefixtures("init_persistence") -def test_calc_profit(limit_buy_order, limit_sell_order, fee): +def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee): + """ + 10 minute limit trade on Binance/Kraken at 1x, 3x leverage + arguments: + fee: + 0.25% quote + 0.30% quote + interest_rate: 0.05% per 4 hrs + open_rate: 2.0 quote + close_rate: + 1.9 quote + 2.1 quote + 2.2 quote + amount: = 30.0 crypto + stake_amount + 1x,-1x: 60.0 quote + 3x,-3x: 20.0 quote + hours: 1/6 (10 minutes) + borrowed + 1x: 0 quote + 3x: 40 quote + -1x: 30 crypto + -3x: 30 crypto + time-periods: + kraken: (1 + 1) 4hr_periods = 2 4hr_periods + binance: 1/24 24hr_periods + interest: borrowed * interest_rate * time-periods + 1x : / + binance 3x: 40 * 0.0005 * 1/24 = 0.0008333333333333334 quote + kraken 3x: 40 * 0.0005 * 2 = 0.040 quote + binace -1x,-3x: 30 * 0.0005 * 1/24 = 0.000625 crypto + kraken -1x,-3x: 30 * 0.0005 * 2 = 0.030 crypto + open_value: (amount * open_rate) ± (amount * open_rate * fee) + 0.0025 fee + 1x, 3x: 30 * 2 + 30 * 2 * 0.0025 = 60.15 quote + -1x,-3x: 30 * 2 - 30 * 2 * 0.0025 = 59.85 quote + 0.003 fee: Is only applied to close rate in this test + amount_closed: + 1x, 3x = amount + -1x, -3x = amount + interest + binance -1x,-3x: 30 + 0.000625 = 30.000625 crypto + kraken -1x,-3x: 30 + 0.03 = 30.03 crypto + close_value: + equations: + 1x, 3x: (amount_closed * close_rate) - (amount_closed * close_rate * fee) - interest + -1x,-3x: (amount_closed * close_rate) + (amount_closed * close_rate * fee) + 2.1 quote + bin,krak 1x: (30.00 * 2.1) - (30.00 * 2.1 * 0.0025) = 62.8425 + bin 3x: (30.00 * 2.1) - (30.00 * 2.1 * 0.0025) - 0.0008333333 = 62.8416666667 + krak 3x: (30.00 * 2.1) - (30.00 * 2.1 * 0.0025) - 0.040 = 62.8025 + bin -1x,-3x: (30.000625 * 2.1) + (30.000625 * 2.1 * 0.0025) = 63.15881578125 + krak -1x,-3x: (30.03 * 2.1) + (30.03 * 2.1 * 0.0025) = 63.2206575 + 1.9 quote + bin,krak 1x: (30.00 * 1.9) - (30.00 * 1.9 * 0.0025) = 56.8575 + bin 3x: (30.00 * 1.9) - (30.00 * 1.9 * 0.0025) - 0.0008333333 = 56.85666667 + krak 3x: (30.00 * 1.9) - (30.00 * 1.9 * 0.0025) - 0.040 = 56.8175 + bin -1x,-3x: (30.000625 * 1.9) + (30.000625 * 1.9 * 0.0025) = 57.14369046875 + krak -1x,-3x: (30.03 * 1.9) + (30.03 * 1.9 * 0.0025) = 57.1996425 + 2.2 quote + bin,krak 1x: (30.00 * 2.20) - (30.00 * 2.20 * 0.0025) = 65.835 + bin 3x: (30.00 * 2.20) - (30.00 * 2.20 * 0.0025) - 0.00083333 = 65.83416667 + krak 3x: (30.00 * 2.20) - (30.00 * 2.20 * 0.0025) - 0.040 = 65.795 + bin -1x,-3x: (30.000625 * 2.20) + (30.000625 * 2.20 * 0.0025) = 66.1663784375 + krak -1x,-3x: (30.03 * 2.20) + (30.03 * 2.20 * 0.0025) = 66.231165 + total_profit: + equations: + 1x, 3x : close_value - open_value + -1x,-3x: open_value - close_value + 2.1 quote + binance,kraken 1x: 62.8425 - 60.15 = 2.6925 + binance 3x: 62.84166667 - 60.15 = 2.69166667 + kraken 3x: 62.8025 - 60.15 = 2.6525 + binance -1x,-3x: 59.850 - 63.15881578125 = -3.308815781249997 + kraken -1x,-3x: 59.850 - 63.2206575 = -3.3706575 + 1.9 quote + binance,kraken 1x: 56.8575 - 60.15 = -3.2925 + binance 3x: 56.85666667 - 60.15 = -3.29333333 + kraken 3x: 56.8175 - 60.15 = -3.3325 + binance -1x,-3x: 59.850 - 57.14369046875 = 2.7063095312499996 + kraken -1x,-3x: 59.850 - 57.1996425 = 2.6503575 + 2.2 quote + binance,kraken 1x: 65.835 - 60.15 = 5.685 + binance 3x: 65.83416667 - 60.15 = 5.68416667 + kraken 3x: 65.795 - 60.15 = 5.645 + binance -1x,-3x: 59.850 - 66.1663784375 = -6.316378437499999 + kraken -1x,-3x: 59.850 - 66.231165 = -6.381165 + total_profit_ratio: + equations: + 1x, 3x : ((close_value/open_value) - 1) * leverage + -1x,-3x: (1 - (close_value/open_value)) * leverage + 2.1 quote + binance,kraken 1x: (62.8425 / 60.15) - 1 = 0.04476309226932673 + binance 3x: ((62.84166667 / 60.15) - 1)*3 = 0.13424771421446402 + kraken 3x: ((62.8025 / 60.15) - 1)*3 = 0.13229426433915248 + binance -1x: 1 - (63.15881578125 / 59.850) = -0.05528514254385963 + binance -3x: (1 - (63.15881578125 / 59.850))*3 = -0.1658554276315789 + kraken -1x: 1 - (63.2206575 / 59.850) = -0.05631842105263152 + kraken -3x: (1 - (63.2206575 / 59.850))*3 = -0.16895526315789455 + 1.9 quote + binance,kraken 1x: (56.8575 / 60.15) - 1 = -0.05473815461346632 + binance 3x: ((56.85666667 / 60.15) - 1)*3 = -0.16425602643391513 + kraken 3x: ((56.8175 / 60.15) - 1)*3 = -0.16620947630922667 + binance -1x: 1 - (57.14369046875 / 59.850) = 0.045218204365079395 + binance -3x: (1 - (57.14369046875 / 59.850))*3 = 0.13565461309523819 + kraken -1x: 1 - (57.1996425 / 59.850) = 0.04428333333333334 + kraken -3x: (1 - (57.1996425 / 59.850))*3 = 0.13285000000000002 + 2.2 quote + binance,kraken 1x: (65.835 / 60.15) - 1 = 0.0945137157107232 + binance 3x: ((65.83416667 / 60.15) - 1)*3 = 0.2834995845386534 + kraken 3x: ((65.795 / 60.15) - 1)*3 = 0.2815461346633419 + binance -1x: 1 - (66.1663784375 / 59.850) = -0.1055368159983292 + binance -3x: (1 - (66.1663784375 / 59.850))*3 = -0.3166104479949876 + kraken -1x: 1 - (66.231165 / 59.850) = -0.106619298245614 + kraken -3x: (1 - (66.231165 / 59.850))*3 = -0.319857894736842 + fee: 0.003, 1x + close_value: + 2.1 quote: (30.00 * 2.1) - (30.00 * 2.1 * 0.003) = 62.811 + 1.9 quote: (30.00 * 1.9) - (30.00 * 1.9 * 0.003) = 56.829 + 2.2 quote: (30.00 * 2.2) - (30.00 * 2.2 * 0.003) = 65.802 + total_profit + fee: 0.003, 1x + 2.1 quote: 62.811 - 60.15 = 2.6610000000000014 + 1.9 quote: 56.829 - 60.15 = -3.320999999999998 + 2.2 quote: 65.802 - 60.15 = 5.652000000000008 + total_profit_ratio + fee: 0.003, 1x + 2.1 quote: (62.811 / 60.15) - 1 = 0.04423940149625927 + 1.9 quote: (56.829 / 60.15) - 1 = -0.05521197007481293 + 2.2 quote: (65.802 / 60.15) - 1 = 0.09396508728179565 + """ trade = Trade( pair='ETH/BTC', - stake_amount=0.001, - amount=5, - open_rate=0.00001099, + stake_amount=60.0, + amount=30.0, + open_rate=2.0, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10), + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='binance', + exchange='binance' ) trade.open_order_id = 'something' - trade.update(limit_buy_order) # Buy @ 0.00001099 + trade.update(limit_buy_order_usdt) # Buy @ 2.0 + # 1x Leverage, long # Custom closing rate and regular fee rate - # Higher than open rate - assert trade.calc_profit(rate=0.00001234) == 0.00011753 - # Lower than open rate - assert trade.calc_profit(rate=0.00000123) == -0.00089086 + # Higher than open rate - 2.1 quote + assert trade.calc_profit(rate=2.1) == 2.6925 + # Lower than open rate - 1.9 quote + assert trade.calc_profit(rate=1.9) == round(-3.292499999999997, 8) - # Custom closing rate and custom fee rate - # Higher than open rate - assert trade.calc_profit(rate=0.00001234, fee=0.003) == 0.00011697 - # Lower than open rate - assert trade.calc_profit(rate=0.00000123, fee=0.003) == -0.00089092 + # fee 0.003 + # Higher than open rate - 2.1 quote + assert trade.calc_profit(rate=2.1, fee=0.003) == 2.661 + # Lower than open rate - 1.9 quote + assert trade.calc_profit(rate=1.9, fee=0.003) == round(-3.320999999999998, 8) - # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 - trade.update(limit_sell_order) - assert trade.calc_profit() == 0.00006217 + # Test when we apply a Sell order. Sell higher than open rate @ 2.2 + trade.update(limit_sell_order_usdt) + assert trade.calc_profit() == round(5.684999999999995, 8) # Test with a custom fee rate on the close trade - assert trade.calc_profit(fee=0.003) == 0.00006163 + assert trade.calc_profit(fee=0.003) == round(5.652000000000008, 8) + + trade.open_trade_value = 0.0 + trade.open_trade_value = trade._calc_open_trade_value() + + # 3x leverage, long ################################################### + trade.leverage = 3.0 + # Higher than open rate - 2.1 quote + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit(rate=2.1, fee=0.0025) == 2.69166667 + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit(rate=2.1, fee=0.0025) == 2.6525 + + # 1.9 quote + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit(rate=1.9, fee=0.0025) == -3.29333333 + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit(rate=1.9, fee=0.0025) == -3.3325 + + # 2.2 quote + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit(fee=0.0025) == 5.68416667 + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit(fee=0.0025) == 5.645 + + # 3x leverage, short ################################################### + trade.set_is_short(True) + # 2.1 quote - Higher than open rate + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit(rate=2.1, fee=0.0025) == round(-3.308815781249997, 8) + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit(rate=2.1, fee=0.0025) == -3.3706575 + + # 1.9 quote - Lower than open rate + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit(rate=1.9, fee=0.0025) == round(2.7063095312499996, 8) + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit(rate=1.9, fee=0.0025) == 2.6503575 + + # Test when we apply a Sell order. Uses sell order used above + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit(fee=0.0025) == round(-6.316378437499999, 8) + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit(fee=0.0025) == -6.381165 + + # 1x leverage, short ################################################### + trade.leverage = 1.0 + # 2.1 quote - Higher than open rate + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit(rate=2.1, fee=0.0025) == round(-3.308815781249997, 8) + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit(rate=2.1, fee=0.0025) == -3.3706575 + + # 1.9 quote - Lower than open rate + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit(rate=1.9, fee=0.0025) == round(2.7063095312499996, 8) + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit(rate=1.9, fee=0.0025) == 2.6503575 + + # Test when we apply a Sell order. Uses sell order used above + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit(fee=0.0025) == round(-6.316378437499999, 8) + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit(fee=0.0025) == -6.381165 @pytest.mark.usefixtures("init_persistence") -def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee): +def test_calc_profit_ratio(limit_buy_order_usdt, limit_sell_order_usdt, fee): trade = Trade( pair='ETH/BTC', - stake_amount=0.001, - amount=5, - open_rate=0.00001099, + stake_amount=60.0, + amount=30.0, + open_rate=2.0, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10), + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='binance', + exchange='binance' ) trade.open_order_id = 'something' - trade.update(limit_buy_order) # Buy @ 0.00001099 + trade.update(limit_buy_order_usdt) # Buy @ 2.0 - # Get percent of profit with a custom rate (Higher than open rate) - assert trade.calc_profit_ratio(rate=0.00001234) == 0.11723875 + # 1x Leverage, long + # Custom closing rate and regular fee rate + # Higher than open rate - 2.1 quote + assert trade.calc_profit_ratio(rate=2.1) == round(0.04476309226932673, 8) + # Lower than open rate - 1.9 quote + assert trade.calc_profit_ratio(rate=1.9) == round(-0.05473815461346632, 8) - # Get percent of profit with a custom rate (Lower than open rate) - assert trade.calc_profit_ratio(rate=0.00000123) == -0.88863828 + # fee 0.003 + # Higher than open rate - 2.1 quote + assert trade.calc_profit_ratio(rate=2.1, fee=0.003) == round(0.04423940149625927, 8) + # Lower than open rate - 1.9 quote + assert trade.calc_profit_ratio(rate=1.9, fee=0.003) == round(-0.05521197007481293, 8) - # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 - trade.update(limit_sell_order) - assert trade.calc_profit_ratio() == 0.06201058 + # Test when we apply a Sell order. Sell higher than open rate @ 2.2 + trade.update(limit_sell_order_usdt) + assert trade.calc_profit_ratio() == round(0.0945137157107232, 8) # Test with a custom fee rate on the close trade - assert trade.calc_profit_ratio(fee=0.003) == 0.06147824 + assert trade.calc_profit_ratio(fee=0.003) == round(0.09396508728179565, 8) trade.open_trade_value = 0.0 assert trade.calc_profit_ratio(fee=0.003) == 0.0 + trade.open_trade_value = trade._calc_open_trade_value() + + # 3x leverage, long ################################################### + trade.leverage = 3.0 + # 2.1 quote - Higher than open rate + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit_ratio(rate=2.1) == round(0.13424771421446402, 8) + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit_ratio(rate=2.1) == round(0.13229426433915248, 8) + + # 1.9 quote - Lower than open rate + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit_ratio(rate=1.9) == round(-0.16425602643391513, 8) + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit_ratio(rate=1.9) == round(-0.16620947630922667, 8) + + # Test when we apply a Sell order. Uses sell order used above + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit_ratio() == round(0.2834995845386534, 8) + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit_ratio() == round(0.2815461346633419, 8) + + # 3x leverage, short ################################################### + trade.set_is_short(True) + # 2.1 quote - Higher than open rate + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit_ratio(rate=2.1) == round(-0.1658554276315789, 8) + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit_ratio(rate=2.1) == round(-0.16895526315789455, 8) + + # 1.9 quote - Lower than open rate + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit_ratio(rate=1.9) == round(0.13565461309523819, 8) + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit_ratio(rate=1.9) == round(0.13285000000000002, 8) + + # Test when we apply a Sell order. Uses sell order used above + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit_ratio() == round(-0.3166104479949876, 8) + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit_ratio() == round(-0.319857894736842, 8) + + # 1x leverage, short ################################################### + trade.leverage = 1.0 + # 2.1 quote - Higher than open rate + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit_ratio(rate=2.1) == round(-0.05528514254385963, 8) + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit_ratio(rate=2.1) == round(-0.05631842105263152, 8) + + # 1.9 quote - Lower than open rate + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit_ratio(rate=1.9) == round(0.045218204365079395, 8) + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit_ratio(rate=1.9) == round(0.04428333333333334, 8) + + # Test when we apply a Sell order. Uses sell order used above + trade.interest_mode = InterestMode.HOURSPERDAY # binance + assert trade.calc_profit_ratio() == round(-0.1055368159983292, 8) + trade.interest_mode = InterestMode.HOURSPER4 # kraken + assert trade.calc_profit_ratio() == round(-0.106619298245614, 8) @pytest.mark.usefixtures("init_persistence") @@ -812,7 +1451,6 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert orders[1].order_id == 'stop_order_id222' assert orders[1].ft_order_side == 'stoploss' - # assert orders[0].is_short is False def test_migrate_mid_state(mocker, default_conf, fee, caplog): @@ -930,6 +1568,60 @@ def test_adjust_stop_loss(fee): assert trade.stop_loss_pct == -0.1 +def test_adjust_stop_loss_short(fee): + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + open_rate=1, + max_rate=1, + is_short=True, + interest_mode=InterestMode.HOURSPERDAY + ) + trade.adjust_stop_loss(trade.open_rate, 0.05, True) + assert trade.stop_loss == 1.05 + assert trade.stop_loss_pct == 0.05 + assert trade.initial_stop_loss == 1.05 + assert trade.initial_stop_loss_pct == 0.05 + # Get percent of profit with a lower rate + trade.adjust_stop_loss(1.04, 0.05) + assert trade.stop_loss == 1.05 + assert trade.stop_loss_pct == 0.05 + assert trade.initial_stop_loss == 1.05 + assert trade.initial_stop_loss_pct == 0.05 + # Get percent of profit with a custom rate (Higher than open rate) + trade.adjust_stop_loss(0.7, 0.1) + # If the price goes down to 0.7, with a trailing stop of 0.1, + # the new stoploss at 0.1 above 0.7 would be 0.7*0.1 higher + assert round(trade.stop_loss, 8) == 0.77 + assert trade.stop_loss_pct == 0.1 + assert trade.initial_stop_loss == 1.05 + assert trade.initial_stop_loss_pct == 0.05 + # current rate lower again ... should not change + trade.adjust_stop_loss(0.8, -0.1) + assert round(trade.stop_loss, 8) == 0.77 + assert trade.initial_stop_loss == 1.05 + assert trade.initial_stop_loss_pct == 0.05 + # current rate higher... should raise stoploss + trade.adjust_stop_loss(0.6, -0.1) + assert round(trade.stop_loss, 8) == 0.66 + assert trade.initial_stop_loss == 1.05 + assert trade.initial_stop_loss_pct == 0.05 + # Initial is true but stop_loss set - so doesn't do anything + trade.adjust_stop_loss(0.3, -0.1, True) + assert round(trade.stop_loss, 8) == 0.66 # TODO-mg: What is this test? + assert trade.initial_stop_loss == 1.05 + assert trade.initial_stop_loss_pct == 0.05 + assert trade.stop_loss_pct == 0.1 + trade.set_liquidation_price(0.63) + trade.adjust_stop_loss(0.59, -0.1) + assert trade.stop_loss == 0.63 + assert trade.liquidation_price == 0.63 + + def test_adjust_min_max_rates(fee): trade = Trade( pair='ETH/BTC', @@ -973,6 +1665,18 @@ def test_get_open(fee, use_db): Trade.use_db = True +@pytest.mark.usefixtures("init_persistence") +@pytest.mark.parametrize('use_db', [True, False]) +def test_get_open_lev(fee, use_db): + Trade.use_db = use_db + Trade.reset_trades() + + create_mock_trades_with_leverage(fee, use_db) + assert len(Trade.get_open_trades()) == 5 + + Trade.use_db = True + + @pytest.mark.usefixtures("init_persistence") def test_to_json(default_conf, fee): @@ -1174,6 +1878,66 @@ def test_stoploss_reinitialization(default_conf, fee): assert trade_adj.initial_stop_loss_pct == -0.04 +def test_stoploss_reinitialization_short(default_conf, fee): + init_db(default_conf['db_url']) + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + fee_open=fee.return_value, + open_date=arrow.utcnow().shift(hours=-2).datetime, + amount=10, + fee_close=fee.return_value, + exchange='binance', + open_rate=1, + max_rate=1, + is_short=True, + leverage=3.0, + interest_mode=InterestMode.HOURSPERDAY + ) + trade.adjust_stop_loss(trade.open_rate, -0.05, True) + assert trade.stop_loss == 1.05 + assert trade.stop_loss_pct == 0.05 + assert trade.initial_stop_loss == 1.05 + assert trade.initial_stop_loss_pct == 0.05 + Trade.query.session.add(trade) + # Lower stoploss + Trade.stoploss_reinitialization(-0.06) + trades = Trade.get_open_trades() + assert len(trades) == 1 + trade_adj = trades[0] + assert trade_adj.stop_loss == 1.06 + assert trade_adj.stop_loss_pct == 0.06 + assert trade_adj.initial_stop_loss == 1.06 + assert trade_adj.initial_stop_loss_pct == 0.06 + # Raise stoploss + Trade.stoploss_reinitialization(-0.04) + trades = Trade.get_open_trades() + assert len(trades) == 1 + trade_adj = trades[0] + assert trade_adj.stop_loss == 1.04 + assert trade_adj.stop_loss_pct == 0.04 + assert trade_adj.initial_stop_loss == 1.04 + assert trade_adj.initial_stop_loss_pct == 0.04 + # Trailing stoploss + trade.adjust_stop_loss(0.98, -0.04) + assert trade_adj.stop_loss == 1.0192 + assert trade_adj.initial_stop_loss == 1.04 + Trade.stoploss_reinitialization(-0.04) + trades = Trade.get_open_trades() + assert len(trades) == 1 + trade_adj = trades[0] + # Stoploss should not change in this case. + assert trade_adj.stop_loss == 1.0192 + assert trade_adj.stop_loss_pct == 0.04 + assert trade_adj.initial_stop_loss == 1.04 + assert trade_adj.initial_stop_loss_pct == 0.04 + # Stoploss can't go above liquidation price + trade_adj.set_liquidation_price(1.0) + trade.adjust_stop_loss(0.97, -0.04) + assert trade_adj.stop_loss == 1.0 + assert trade_adj.stop_loss == 1.0 + + def test_update_fee(fee): trade = Trade( pair='ETH/BTC', @@ -1331,6 +2095,19 @@ def test_get_best_pair(fee): assert res[1] == 0.01 +@pytest.mark.usefixtures("init_persistence") +def test_get_best_pair_lev(fee): + + res = Trade.get_best_pair() + assert res is None + + create_mock_trades_with_leverage(fee) + res = Trade.get_best_pair() + assert len(res) == 2 + assert res[0] == 'DOGE/BTC' + assert res[1] == 0.1713156134055116 + + @pytest.mark.usefixtures("init_persistence") def test_update_order_from_ccxt(caplog): # Most basic order return (only has orderid) From 4fcae0d9275553504aed8b29539adbee1e1d25d7 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 25 Jul 2021 02:57:17 -0600 Subject: [PATCH 45/46] Changed liquidation_price to isolated_liq --- freqtrade/persistence/migrations.py | 6 ++-- freqtrade/persistence/models.py | 36 +++++++++++----------- tests/rpc/test_rpc.py | 4 +-- tests/test_persistence.py | 48 ++++++++++++++--------------- 4 files changed, 47 insertions(+), 47 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 39997a8f4..8077a3a49 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -50,7 +50,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col leverage = get_column_def(cols, 'leverage', '1.0') interest_rate = get_column_def(cols, 'interest_rate', '0.0') - liquidation_price = get_column_def(cols, 'liquidation_price', 'null') + isolated_liq = get_column_def(cols, 'isolated_liq', 'null') # sqlite does not support literals for booleans is_short = get_column_def(cols, 'is_short', '0') interest_mode = get_column_def(cols, 'interest_mode', 'null') @@ -90,7 +90,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stoploss_order_id, stoploss_last_update, max_rate, min_rate, sell_reason, sell_order_status, strategy, timeframe, open_trade_value, close_profit_abs, - leverage, interest_rate, liquidation_price, is_short, interest_mode + leverage, interest_rate, isolated_liq, is_short, interest_mode ) select id, lower(exchange), case @@ -115,7 +115,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {strategy} strategy, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, {leverage} leverage, {interest_rate} interest_rate, - {liquidation_price} liquidation_price, {is_short} is_short, + {isolated_liq} isolated_liq, {is_short} is_short, {interest_mode} interest_mode from {table_back_name} """)) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 9e2e99063..83481969f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -265,7 +265,7 @@ class LocalTrade(): # Margin trading properties interest_rate: float = 0.0 - liquidation_price: Optional[float] = None + isolated_liq: Optional[float] = None is_short: bool = False leverage: float = 1.0 interest_mode: InterestMode = InterestMode.NONE @@ -314,8 +314,8 @@ class LocalTrade(): def __init__(self, **kwargs): for key in kwargs: setattr(self, key, kwargs[key]) - if self.liquidation_price: - self.set_liquidation_price(self.liquidation_price) + if self.isolated_liq: + self.set_isolated_liq(self.isolated_liq) self.recalc_open_trade_value() def set_stop_loss(self, stop_loss: float): @@ -323,11 +323,11 @@ class LocalTrade(): Method you should use to set self.stop_loss. Assures stop_loss is not passed the liquidation price """ - if self.liquidation_price is not None: + if self.isolated_liq is not None: if self.is_short: - sl = min(stop_loss, self.liquidation_price) + sl = min(stop_loss, self.isolated_liq) else: - sl = max(stop_loss, self.liquidation_price) + sl = max(stop_loss, self.isolated_liq) else: sl = stop_loss @@ -335,21 +335,21 @@ class LocalTrade(): self.initial_stop_loss = sl self.stop_loss = sl - def set_liquidation_price(self, liquidation_price: float): + def set_isolated_liq(self, isolated_liq: float): """ Method you should use to set self.liquidation price. Assures stop_loss is not passed the liquidation price """ if self.stop_loss is not None: if self.is_short: - self.stop_loss = min(self.stop_loss, liquidation_price) + self.stop_loss = min(self.stop_loss, isolated_liq) else: - self.stop_loss = max(self.stop_loss, liquidation_price) + self.stop_loss = max(self.stop_loss, isolated_liq) else: - self.initial_stop_loss = liquidation_price - self.stop_loss = liquidation_price + self.initial_stop_loss = isolated_liq + self.stop_loss = isolated_liq - self.liquidation_price = liquidation_price + self.isolated_liq = isolated_liq def set_is_short(self, is_short: bool): self.is_short = is_short @@ -425,7 +425,7 @@ class LocalTrade(): 'leverage': self.leverage, 'interest_rate': self.interest_rate, - 'liquidation_price': self.liquidation_price, + 'isolated_liq': self.isolated_liq, 'is_short': self.is_short, 'open_order_id': self.open_order_id, @@ -472,13 +472,13 @@ class LocalTrade(): if self.is_short: new_loss = float(current_price * (1 + abs(stoploss))) # If trading on margin, don't set the stoploss below the liquidation price - if self.liquidation_price: - new_loss = min(self.liquidation_price, new_loss) + if self.isolated_liq: + new_loss = min(self.isolated_liq, new_loss) else: new_loss = float(current_price * (1 - abs(stoploss))) # If trading on margin, don't set the stoploss below the liquidation price - if self.liquidation_price: - new_loss = max(self.liquidation_price, new_loss) + if self.isolated_liq: + new_loss = max(self.isolated_liq, new_loss) # no stop loss assigned yet if not self.stop_loss: @@ -905,7 +905,7 @@ class Trade(_DECL_BASE, LocalTrade): # Margin trading properties leverage = Column(Float, nullable=True, default=1.0) interest_rate = Column(Float, nullable=False, default=0.0) - liquidation_price = Column(Float, nullable=True) + isolated_liq = Column(Float, nullable=True) is_short = Column(Boolean, nullable=False, default=False) interest_mode = Column(Enum(InterestMode), nullable=True) # End of margin trading properties diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 3650aa57b..323a647c1 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -109,7 +109,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'exchange': 'binance', 'leverage': 1.0, 'interest_rate': 0.0, - 'liquidation_price': None, + 'isolated_liq': None, 'is_short': False, } @@ -179,7 +179,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'exchange': 'binance', 'leverage': 1.0, 'interest_rate': 0.0, - 'liquidation_price': None, + 'isolated_liq': None, 'is_short': False, } diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 7c9df6258..512a6c83d 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -112,7 +112,7 @@ def test_is_opening_closing_trade(fee): @pytest.mark.usefixtures("init_persistence") -def test_set_stop_loss_liquidation_price(fee): +def test_set_stop_loss_isolated_liq(fee): trade = Trade( id=2, pair='ETH/BTC', @@ -127,36 +127,36 @@ def test_set_stop_loss_liquidation_price(fee): is_short=False, leverage=2.0 ) - trade.set_liquidation_price(0.09) - assert trade.liquidation_price == 0.09 + trade.set_isolated_liq(0.09) + assert trade.isolated_liq == 0.09 assert trade.stop_loss == 0.09 assert trade.initial_stop_loss == 0.09 trade.set_stop_loss(0.1) - assert trade.liquidation_price == 0.09 + assert trade.isolated_liq == 0.09 assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.09 - trade.set_liquidation_price(0.08) - assert trade.liquidation_price == 0.08 + trade.set_isolated_liq(0.08) + assert trade.isolated_liq == 0.08 assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.09 - trade.set_liquidation_price(0.11) - assert trade.liquidation_price == 0.11 + trade.set_isolated_liq(0.11) + assert trade.isolated_liq == 0.11 assert trade.stop_loss == 0.11 assert trade.initial_stop_loss == 0.09 trade.set_stop_loss(0.1) - assert trade.liquidation_price == 0.11 + assert trade.isolated_liq == 0.11 assert trade.stop_loss == 0.11 assert trade.initial_stop_loss == 0.09 trade.stop_loss = None - trade.liquidation_price = None + trade.isolated_liq = None trade.initial_stop_loss = None trade.set_stop_loss(0.07) - assert trade.liquidation_price is None + assert trade.isolated_liq is None assert trade.stop_loss == 0.07 assert trade.initial_stop_loss == 0.07 @@ -164,28 +164,28 @@ def test_set_stop_loss_liquidation_price(fee): trade.stop_loss = None trade.initial_stop_loss = None - trade.set_liquidation_price(0.09) - assert trade.liquidation_price == 0.09 + trade.set_isolated_liq(0.09) + assert trade.isolated_liq == 0.09 assert trade.stop_loss == 0.09 assert trade.initial_stop_loss == 0.09 trade.set_stop_loss(0.08) - assert trade.liquidation_price == 0.09 + assert trade.isolated_liq == 0.09 assert trade.stop_loss == 0.08 assert trade.initial_stop_loss == 0.09 - trade.set_liquidation_price(0.1) - assert trade.liquidation_price == 0.1 + trade.set_isolated_liq(0.1) + assert trade.isolated_liq == 0.1 assert trade.stop_loss == 0.08 assert trade.initial_stop_loss == 0.09 - trade.set_liquidation_price(0.07) - assert trade.liquidation_price == 0.07 + trade.set_isolated_liq(0.07) + assert trade.isolated_liq == 0.07 assert trade.stop_loss == 0.07 assert trade.initial_stop_loss == 0.09 trade.set_stop_loss(0.1) - assert trade.liquidation_price == 0.07 + assert trade.isolated_liq == 0.07 assert trade.stop_loss == 0.07 assert trade.initial_stop_loss == 0.09 @@ -1616,10 +1616,10 @@ def test_adjust_stop_loss_short(fee): assert trade.initial_stop_loss == 1.05 assert trade.initial_stop_loss_pct == 0.05 assert trade.stop_loss_pct == 0.1 - trade.set_liquidation_price(0.63) + trade.set_isolated_liq(0.63) trade.adjust_stop_loss(0.59, -0.1) assert trade.stop_loss == 0.63 - assert trade.liquidation_price == 0.63 + assert trade.isolated_liq == 0.63 def test_adjust_min_max_rates(fee): @@ -1744,7 +1744,7 @@ def test_to_json(default_conf, fee): 'exchange': 'binance', 'leverage': None, 'interest_rate': None, - 'liquidation_price': None, + 'isolated_liq': None, 'is_short': None, } @@ -1813,7 +1813,7 @@ def test_to_json(default_conf, fee): 'exchange': 'binance', 'leverage': None, 'interest_rate': None, - 'liquidation_price': None, + 'isolated_liq': None, 'is_short': None, } @@ -1932,7 +1932,7 @@ def test_stoploss_reinitialization_short(default_conf, fee): assert trade_adj.initial_stop_loss == 1.04 assert trade_adj.initial_stop_loss_pct == 0.04 # Stoploss can't go above liquidation price - trade_adj.set_liquidation_price(1.0) + trade_adj.set_isolated_liq(1.0) trade.adjust_stop_loss(0.97, -0.04) assert trade_adj.stop_loss == 1.0 assert trade_adj.stop_loss == 1.0 From 10d214ccadb02bb257c1fffffff8489b71ce6783 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 26 Jul 2021 22:41:45 -0600 Subject: [PATCH 46/46] Added is_short and leverage to __repr__ --- freqtrade/persistence/models.py | 9 +++++-- tests/test_freqtradebot.py | 42 +++++++++++++++++++++------------ tests/test_persistence.py | 35 +++++++++++++++++++++++---- 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 83481969f..6f5cc590e 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -357,9 +357,14 @@ class LocalTrade(): def __repr__(self): open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed' + 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'open_rate={self.open_rate:.8f}, open_since={open_since})') + 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})' + ) def to_json(self) -> Dict[str, Any]: return { diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 4912a2a4d..8742228ac 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2401,6 +2401,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke freqtrade.check_handle_timedout() assert log_has_re(r"Cannot query order for Trade\(id=1, pair=ETH/BTC, amount=90.99181073, " + r"is_short=False, leverage=1.0, " r"open_rate=0.00001099, open_since=" f"{open_trade.open_date.strftime('%Y-%m-%d %H:%M:%S')}" r"\) due to Traceback \(most recent call last\):\n*", @@ -3549,9 +3550,11 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, fe # Amount is reduced by "fee" assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001) - assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' - 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).', - caplog) + assert log_has( + 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,' + ' leverage=1.0, open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).', + caplog + ) def test_get_real_amount_quote_dust(default_conf, trades_for_order, buy_order_fee, fee, @@ -3596,9 +3599,12 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker, f # Amount is reduced by "fee" assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' - 'open_rate=0.24544100, open_since=closed) failed: myTrade-Dict empty found', - caplog) + assert log_has( + 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' + 'is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed) failed: ' + 'myTrade-Dict empty found', + caplog + ) def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, fee, mocker): @@ -3682,9 +3688,11 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c # Amount is reduced by "fee" assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001) - assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' - 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).', - caplog) + assert log_has( + 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,' + ' leverage=1.0, open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).', + caplog + ) assert trade.fee_open == 0.001 assert trade.fee_close == 0.001 @@ -3718,9 +3726,11 @@ def test_get_real_amount_multi2(default_conf, trades_for_order3, buy_order_fee, # Amount is reduced by "fee" assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.0005) - assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' - 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).', - caplog) + assert log_has( + 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,' + ' leverage=1.0, open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).', + caplog + ) # Overall fee is average of both trade's fee assert trade.fee_open == 0.001518575 assert trade.fee_open_cost is not None @@ -3752,9 +3762,11 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee # Amount is reduced by "fee" assert freqtrade.get_real_amount(trade, limit_buy_order) == amount - 0.004 - assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' - 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).', - caplog) + assert log_has( + 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,' + ' leverage=1.0, open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).', + caplog + ) def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order_fee, fee, mocker): diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 512a6c83d..cf9c38cfa 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -520,7 +520,8 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca assert trade.close_profit is None assert trade.close_date is None assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, " - r"pair=ETH/BTC, amount=30.00000000, open_rate=2.00000000, open_since=.*\).", + r'pair=ETH/BTC, amount=30.00000000, ' + r"is_short=False, leverage=1.0, open_rate=2.00000000, open_since=.*\).", caplog) caplog.clear() @@ -531,7 +532,31 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca assert trade.close_profit == round(0.0945137157107232, 8) assert trade.close_date is not None assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, " - r"pair=ETH/BTC, amount=30.00000000, open_rate=2.00000000, open_since=.*\).", + r"pair=ETH/BTC, amount=30.00000000, " + r"is_short=False, leverage=1.0, open_rate=2.00000000, open_since=.*\).", + caplog) + caplog.clear() + + trade = Trade( + id=226531, + pair='ETH/BTC', + stake_amount=60.0, + open_rate=2.0, + amount=30.0, + is_open=True, + open_date=arrow.utcnow().datetime, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + is_short=True, + leverage=3.0, + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY + ) + trade.update(limit_buy_order_usdt) + assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=226531, " + r"pair=ETH/BTC, amount=30.00000000, " + r"is_short=True, leverage=3.0, open_rate=2.00000000, open_since=.*\).", caplog) @@ -557,7 +582,8 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, assert trade.close_profit is None assert trade.close_date is None assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, " - r"pair=ETH/BTC, amount=30.00000000, open_rate=2.00000000, open_since=.*\).", + r"pair=ETH/BTC, amount=30.00000000, is_short=False, leverage=1.0, " + r"open_rate=2.00000000, open_since=.*\).", caplog) caplog.clear() @@ -569,7 +595,8 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, assert trade.close_profit == round(0.0945137157107232, 8) assert trade.close_date is not None assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " - r"pair=ETH/BTC, amount=30.00000000, open_rate=2.00000000, open_since=.*\).", + r"pair=ETH/BTC, amount=30.00000000, is_short=False, leverage=1.0, " + r"open_rate=2.00000000, open_since=.*\).", caplog)