diff --git a/docs/leverage.md b/docs/leverage.md new file mode 100644 index 000000000..c4b975a0b --- /dev/null +++ b/docs/leverage.md @@ -0,0 +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/__init__.py b/freqtrade/enums/__init__.py index d803baf31..6099f7003 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..89c71a8b4 --- /dev/null +++ b/freqtrade/enums/interestmode.py @@ -0,0 +1,28 @@ +from decimal import Decimal +from enum import Enum +from math import ceil + +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, borrowed: Decimal, rate: Decimal, hours: Decimal): + + if self.name == "HOURSPERDAY": + return borrowed * rate * ceil(hours)/twenty_four + elif self.name == "HOURSPER4": + # 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/freqtradebot.py b/freqtrade/freqtradebot.py index 09aa06adf..179c99d2c 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/migrations.py b/freqtrade/persistence/migrations.py index 1839c4130..03f412724 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -48,6 +48,13 @@ 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') buy_tag = get_column_def(cols, 'buy_tag', 'null') + + leverage = get_column_def(cols, 'leverage', '1.0') + interest_rate = get_column_def(cols, 'interest_rate', '0.0') + 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') # If ticker-interval existed use that, else null. if has_column(cols, 'ticker_interval'): timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') @@ -59,6 +66,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}") + # 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') @@ -83,7 +91,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, sell_reason, sell_order_status, strategy, buy_tag, - timeframe, open_trade_value, close_profit_abs + timeframe, open_trade_value, close_profit_abs, + leverage, interest_rate, isolated_liq, is_short, interest_mode ) select id, lower(exchange), pair, is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost, @@ -99,7 +108,10 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {sell_order_status} sell_order_status, {strategy} strategy, {buy_tag} buy_tag, {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, {interest_rate} interest_rate, + {isolated_liq} isolated_liq, {is_short} is_short, + {interest_mode} interest_mode from {table_back_name} """)) @@ -134,14 +146,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') + # sqlite does not support literals for booleans 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) + 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} leverage from {table_back_name} """)) @@ -157,7 +171,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None: table_back_name = get_backup_name(tabs, 'trades_bak') # Check for latest column - if not has_column(cols, 'buy_tag'): + if not has_column(cols, 'is_short'): 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! @@ -170,9 +184,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 43fbec8c0..d09c5ed68 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 @@ -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 @@ -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 = {} @@ -132,6 +132,8 @@ 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) + def __repr__(self): return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' @@ -155,6 +157,8 @@ 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) @@ -232,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 @@ -260,16 +264,31 @@ class LocalTrade(): buy_tag: Optional[str] = None timeframe: Optional[int] = None - def __init__(self, **kwargs): - for key in kwargs: - setattr(self, key, kwargs[key]) - self.recalc_open_trade_value() + # Margin trading properties + interest_rate: float = 0.0 + isolated_liq: Optional[float] = None + is_short: bool = False + leverage: float = 1.0 + interest_mode: InterestMode = InterestMode.NONE - def __repr__(self): - open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed' + @property + def has_no_leverage(self) -> bool: + """Returns true if this is a non-leverage, non-short trade""" + return ((self.leverage or self.leverage is None) == 1.0 and not self.is_short) - 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 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: + return (self.amount * self.open_rate) * ((self.leverage-1)/self.leverage) + else: + return self.amount @property def open_date_utc(self): @@ -279,6 +298,77 @@ 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]) + if self.isolated_liq: + self.set_isolated_liq(self.isolated_liq) + self.recalc_open_trade_value() + + def _set_stop_loss(self, stop_loss: float, percent: float): + """ + Method you should use to set self.stop_loss. + Assures stop_loss is not passed the liquidation price + """ + if self.isolated_liq is not None: + if self.is_short: + sl = min(stop_loss, self.isolated_liq) + else: + sl = max(stop_loss, self.isolated_liq) + else: + sl = stop_loss + + if not self.stop_loss: + self.initial_stop_loss = sl + self.stop_loss = sl + + if self.is_short: + self.stop_loss_pct = abs(percent) + else: + self.stop_loss_pct = -1 * abs(percent) + self.stoploss_last_update = datetime.utcnow() + + 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, isolated_liq) + else: + self.stop_loss = max(self.stop_loss, isolated_liq) + else: + self.initial_stop_loss = isolated_liq + self.stop_loss = isolated_liq + + self.isolated_liq = isolated_liq + + 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'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 { 'trade_id': self.id, @@ -342,6 +432,11 @@ class LocalTrade(): 'min_rate': self.min_rate, 'max_rate': self.max_rate, + 'leverage': self.leverage, + 'interest_rate': self.interest_rate, + 'isolated_liq': self.isolated_liq, + 'is_short': self.is_short, + 'open_order_id': self.open_order_id, } @@ -361,12 +456,6 @@ class LocalTrade(): self.max_rate = max(current_price, self.max_rate or self.open_rate) self.min_rate = min(current_price, self.min_rate or self.open_rate) - def _set_new_stoploss(self, new_loss: float, stoploss: float): - """Assign new stop value""" - self.stop_loss = new_loss - self.stop_loss_pct = -1 * abs(stoploss) - self.stoploss_last_update = datetime.utcnow() - def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False) -> None: """ @@ -380,20 +469,39 @@ class LocalTrade(): # Don't modify if called with initial and nothing to do return - new_loss = float(current_price * (1 - abs(stoploss))) + 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.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.isolated_liq: + new_loss = max(self.isolated_liq, 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._set_stop_loss(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: - if new_loss > self.stop_loss: # stop losses only walk up, never down! + + 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 (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) + self._set_stop_loss(new_loss, stoploss) else: logger.debug(f"{self.pair} - Keeping current stoploss...") @@ -412,24 +520,35 @@ class LocalTrade(): :return: None """ order_type = order['type'] + + if 'is_short' in order and order['side'] == 'sell': + # Only set's is_short on opening trades, ignores non-shorts + 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 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.enter_side == 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 'leverage' in order: + self.leverage = order['leverage'] 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.is_short else "BUY" + logger.info(f'{order_type.upper()}_{payment} 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.exit_side == 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.is_short else "SELL" + # 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-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 @@ -447,9 +566,9 @@ 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.open_order_id = None @@ -464,14 +583,14 @@ class LocalTrade(): """ Update Fee parameters. Only acts once per side """ - if side == 'buy' and self.fee_open_currency is None: + if self.enter_side == 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.exit_side == 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: @@ -481,9 +600,9 @@ class LocalTrade(): """ Verify if this side (buy / sell) has already been updated """ - if side == 'buy': + if self.enter_side == side: return self.fee_open_currency is not None - elif side == 'sell': + elif self.exit_side == side: return self.fee_close_currency is not None else: return False @@ -496,67 +615,129 @@ 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.is_short: + return float(open_trade - fees) + else: + return float(open_trade + fees) 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() + 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 + """ + + zero = Decimal(0.0) + # If nothing was borrowed + if self.has_no_leverage: + return zero + + open_date = self.open_date.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 + + rate = Decimal(interest_rate or self.interest_rate) + borrowed = Decimal(self.borrowed) + + return self.interest_mode(borrowed=borrowed, rate=rate, hours=hours) + 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 - 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) + interest = self.calculate_interest(interest_rate) + if self.is_short: + amount = Decimal(self.amount) + Decimal(interest) + else: + # 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 + fees = close_trade * Decimal(fee or self.fee_close) + + if self.is_short: + return float(close_trade + fees) + else: + 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 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 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) ) - profit = close_trade_value - self.open_trade_value + + if self.is_short: + 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, + 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.open_trade_value == 0.0: + + 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 - profit_ratio = (close_trade_value / self.open_trade_value) - 1 + else: + if self.is_short: + profit_ratio = (1 - (close_trade_value/self.open_trade_value)) * leverage + else: + profit_ratio = ((close_trade_value/self.open_trade_value) - 1) * leverage + return float(f"{profit_ratio:.8f}") def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: @@ -702,12 +883,20 @@ 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) + 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) buy_tag = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) + # Margin trading properties + leverage = Column(Float, nullable=True, default=1.0) + interest_rate = Column(Float, nullable=False, default=0.0) + 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 + def __init__(self, **kwargs): super().__init__(**kwargs) self.recalc_open_trade_value() @@ -794,7 +983,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. 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.py b/tests/conftest.py index 1924e1f95..859c34aae 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) +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) @@ -225,6 +225,43 @@ def create_mock_trades(fee, use_db: bool = True): Trade.query.session.flush() +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() + + @pytest.fixture(autouse=True) def patch_coingekko(mocker) -> None: """ diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index b92b51144..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 @@ -303,3 +304,180 @@ def mock_trade_6(fee): o = Order.parse_from_ccxt_object(mock_order_6_sell(), 'LTC/BTC', 'sell') trade.orders.append(o) return trade + + +def short_order(): + return { + 'id': '1236', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'sell', + 'type': 'limit', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + } + + +def exit_short_order(): + return { + 'id': '12367', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'buy', + 'type': 'limit', + 'price': 0.128, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + } + + +def short_trade(fee): + """ + 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=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.04577376693766946, + # close_profit_abs=-0.6925113200000013, + exchange='binance', + 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), + is_short=True, + interest_mode=InterestMode.HOURSPERDAY + ) + 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': '1237', + 'symbol': 'DOGE/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': '12368', + 'symbol': 'DOGE/BTC', + 'status': 'closed', + 'side': 'sell', + 'type': 'limit', + 'price': 0.128, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + 'leverage': 5.0 + } + + +def leverage_trade(fee): + """ + 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: 615 crypto + stake_amount: 15.129 base + borrowed: 60.516 base + leverage: 5 + 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.090774 + = 78.432426 + total_profit = close_value - open_value + = 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', + stake_amount=15.129, + 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.1713156134055116, + close_profit_abs=2.5983135000000175, + exchange='kraken', + is_open=False, + 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_mode=InterestMode.HOURSPER4 + ) + o = Order.parse_from_ccxt_object(leverage_order(), 'DOGE/BTC', 'sell') + trade.orders.append(o) + o = Order.parse_from_ccxt_object(leverage_order_sell(), 'DOGE/BTC', 'sell') + trade.orders.append(o) + return trade diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 136fa157c..db7ad484c 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -108,6 +108,10 @@ 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, + 'interest_rate': 0.0, + 'isolated_liq': None, + 'is_short': False, } mocker.patch('freqtrade.exchange.Exchange.get_rate', @@ -175,6 +179,10 @@ 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, + 'interest_rate': 0.0, + 'isolated_liq': None, + 'is_short': False, } diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index b1e02a99b..7c37bb269 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2434,6 +2434,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*", @@ -3619,9 +3620,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, @@ -3666,9 +3669,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): @@ -3752,9 +3758,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 @@ -3788,9 +3796,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 @@ -3822,9 +3832,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 f7bcad806..16469f6fc 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -11,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): @@ -65,28 +66,433 @@ def test_init_dryrun_db(default_conf, tmpdir): @pytest.mark.usefixtures("init_persistence") -def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, caplog): +def test_enter_exit_side(fee): + trade = Trade( + id=2, + pair='ADA/USDT', + 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.enter_side == 'buy' + assert trade.exit_side == 'sell' + + trade.is_short = True + + assert trade.enter_side == 'sell' + assert trade.exit_side == 'buy' + + +@pytest.mark.usefixtures("init_persistence") +def test__set_stop_loss_isolated_liq(fee): + trade = Trade( + id=2, + pair='ADA/USDT', + 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=False, + leverage=2.0 + ) + 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, (1.0/9.0)) + assert trade.isolated_liq == 0.09 + assert trade.stop_loss == 0.1 + assert trade.initial_stop_loss == 0.09 + + 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_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, 0) + assert trade.isolated_liq == 0.11 + assert trade.stop_loss == 0.11 + assert trade.initial_stop_loss == 0.09 + + trade.stop_loss = None + trade.isolated_liq = None + trade.initial_stop_loss = None + + trade._set_stop_loss(0.07, 0) + assert trade.isolated_liq is None + assert trade.stop_loss == 0.07 + assert trade.initial_stop_loss == 0.07 + + trade.is_short = True + trade.recalc_open_trade_value() + trade.stop_loss = None + trade.initial_stop_loss = None + + 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, (1.0/9.0)) + assert trade.isolated_liq == 0.09 + assert trade.stop_loss == 0.08 + assert trade.initial_stop_loss == 0.09 + + 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_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, (1.0/8.0)) + assert trade.isolated_liq == 0.07 + assert trade.stop_loss == 0.07 + assert trade.initial_stop_loss == 0.09 + + +@pytest.mark.usefixtures("init_persistence") +def test_interest(market_buy_order_usdt, fee): """ - On this test we will buy and sell a crypto currency. - fee: 0.25% quote + 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 - 60.0 quote + 3x, -3x: 20.0 quote + 5x, -5x: 12.0 quote borrowed - 0 quote - open_value: (amount * open_rate) + (amount * open_rate * fee) - 30 * 2 + 30 * 2 * 0.0025 = 60.15 quote + 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 + """ + + trade = Trade( + pair='ADA/USDT', + 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 + ) + + # 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.is_short = True + trade.recalc_open_trade_value() + # 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) + + # 5hr, long + trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55) + trade.is_short = False + trade.recalc_open_trade_value() + # 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.is_short = True + trade.recalc_open_trade_value() + # 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 + + # 0.00025 interest, 5hr, long + trade.is_short = False + trade.recalc_open_trade_value() + # 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.is_short = True + trade.recalc_open_trade_value() + # 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.is_short = False + trade.recalc_open_trade_value() + 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.is_short = True + trade.recalc_open_trade_value() + # 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.is_short = False + trade.recalc_open_trade_value() + 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.is_short = True + trade.recalc_open_trade_value() + # 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: - (amount * close_rate) - (amount * close_rate * fee) - interest - (30.00 * 2.20) - (30.00 * 2.20 * 0.0025) = 65.835 + 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: - close_value - open_value - 65.835 - 60.15 = 5.685 + 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: - ((close_value/open_value) - 1) * leverage - ((65.835 / 60.15) - 1) * 1 = 0.0945137157107232 + 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='ADA/USDT', + 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.is_short = True + trade.recalc_open_trade_value() + assert trade.borrowed == 30.0 + trade.leverage = 3.0 + assert trade.borrowed == 30.0 + trade.is_short = False + trade.recalc_open_trade_value() + 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 + open_rate: 2.2, close_rate: 2.0, -3x, binance, short + open_value: 30 * 2.2 - 30 * 2.2 * 0.0025 = 65.835 quote + amount_closed: 30 + 0.000625 = 30.000625 crypto + close_value: (30.000625 * 2.0) + (30.000625 * 2.0 * 0.0025) = 60.151253125 + total_profit: 65.835 - 60.151253125 = 5.683746874999997 + total_profit_ratio: (1-(60.151253125/65.835)) * 3 = 0.2589996297562085 """ @@ -113,7 +519,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=ADA/USDT, amount=30.00000000, open_rate=2.00000000, open_since=.*\).", + r'pair=ADA/USDT, amount=30.00000000, ' + r"is_short=False, leverage=1.0, open_rate=2.00000000, open_since=.*\).", caplog) caplog.clear() @@ -124,7 +531,50 @@ 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=ADA/USDT, amount=30.00000000, open_rate=2.00000000, open_since=.*\).", + r"pair=ADA/USDT, amount=30.00000000, " + r"is_short=False, leverage=1.0, open_rate=2.00000000, open_since=.*\).", + caplog) + caplog.clear() + + trade = Trade( + id=226531, + pair='ADA/USDT', + stake_amount=20.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.open_order_id = 'something' + trade.update(limit_sell_order_usdt) + + assert trade.open_order_id is None + assert trade.open_rate == 2.20 + 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=226531, " + r"pair=ADA/USDT, amount=30.00000000, " + r"is_short=True, leverage=3.0, open_rate=2.20000000, open_since=.*\).", + caplog) + caplog.clear() + + trade.open_order_id = 'something' + trade.update(limit_buy_order_usdt) + assert trade.open_order_id is None + assert trade.close_rate == 2.00 + assert trade.close_profit == round(0.2589996297562085, 8) + assert trade.close_date is not None + assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=226531, " + r"pair=ADA/USDT, amount=30.00000000, " + r"is_short=True, leverage=3.0, open_rate=2.20000000, open_since=.*\).", caplog) caplog.clear() @@ -151,7 +601,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=ADA/USDT, amount=30.00000000, open_rate=2.00000000, open_since=.*\).", + r"pair=ADA/USDT, amount=30.00000000, is_short=False, leverage=1.0, " + r"open_rate=2.00000000, open_since=.*\).", caplog) caplog.clear() @@ -163,7 +614,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=ADA/USDT, amount=30.00000000, open_rate=2.00000000, open_since=.*\).", + r"pair=ADA/USDT, amount=30.00000000, is_short=False, leverage=1.0, " + r"open_rate=2.00000000, open_since=.*\).", caplog) @@ -174,6 +626,9 @@ def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt 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', @@ -181,15 +636,50 @@ def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt trade.open_order_id = 'something' trade.update(limit_buy_order_usdt) - assert trade._calc_open_trade_value() == 60.15 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) - - # Profit in USDT assert trade.calc_profit() == 5.685 - - # Profit in percent 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.is_short = True + trade.recalc_open_trade_value() + # 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") @@ -202,7 +692,9 @@ def test_trade_close(limit_buy_order_usdt, limit_sell_order_usdt, fee): 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 @@ -281,18 +773,20 @@ def test_update_invalid_order(limit_buy_order_usdt): @pytest.mark.usefixtures("init_persistence") def test_calc_open_trade_value(limit_buy_order_usdt, fee): - """ - fee: 0.25 %, 0.3% quote - open_rate: 2.00 quote - amount: = 30.0 crypto - stake_amount - 60.0 quote - open_value: (amount * open_rate) + (amount * open_rate * fee) - 0.25% fee - 30 * 2 + 30 * 2 * 0.0025 = 60.15 quote - 0.3% fee - 30 * 2 + 30 * 2 * 0.003 = 60.18 quote - """ + # 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='ADA/USDT', stake_amount=60.0, @@ -303,13 +797,27 @@ def test_calc_open_trade_value(limit_buy_order_usdt, fee): exchange='binance', ) trade.open_order_id = 'open_trade' - trade.update(limit_buy_order_usdt) # Buy @ 2.0 + trade.update(limit_buy_order_usdt) # Get the open rate price with the standard fee rate assert trade._calc_open_trade_value() == 60.15 - trade.fee_open = 0.003 + trade.is_short = True + trade.recalc_open_trade_value() + 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.is_short = False + trade.recalc_open_trade_value() + assert trade._calc_open_trade_value() == 60.15 + # Get the open rate price with a custom fee rate + trade.fee_open = 0.003 + assert trade._calc_open_trade_value() == 60.18 + trade.is_short = True + trade.recalc_open_trade_value() + assert trade._calc_open_trade_value() == 59.82 @pytest.mark.usefixtures("init_persistence") @@ -319,29 +827,63 @@ def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee 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_usdt) # Buy @ 2.0 + trade.update(limit_buy_order_usdt) - # Get the close rate price with a custom close rate and a regular fee rate + # 1x leverage binance assert trade.calc_close_trade_value(rate=2.5) == 74.8125 - # Get the close rate price with a custom close rate and a custom fee rate assert trade.calc_close_trade_value(rate=2.5, fee=0.003) == 74.775 - # Test when we apply a Sell order, and ask price with a custom fee rate trade.update(limit_sell_order_usdt) assert trade.calc_close_trade_value(fee=0.005) == 65.67 + # 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 + + # 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.is_short = True + trade.recalc_open_trade_value() + 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_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 @@ -349,51 +891,117 @@ def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee): 2.2 quote amount: = 30.0 crypto stake_amount - 60.0 quote - open_value: (amount * open_rate) + (amount * open_rate * fee) + 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 - 30 * 2 + 30 * 2 * 0.0025 = 60.15 quote - 30 * 2 - 30 * 2 * 0.0025 = 59.85 quote + 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: - (amount_closed * close_rate) - (amount_closed * close_rate * fee) + 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 - (30.00 * 2.1) - (30.00 * 2.1 * 0.0025) = 62.8425 + 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 - (30.00 * 1.9) - (30.00 * 1.9 * 0.0025) = 56.8575 + 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 - (30.00 * 2.20) - (30.00 * 2.20 * 0.0025) = 65.835 + 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: - close_value - open_value + 1x, 3x : close_value - open_value + -1x,-3x: open_value - close_value 2.1 quote - 62.8425 - 60.15 = 2.6925 + 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 - 56.8575 - 60.15 = -3.2925 + 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 - 65.835 - 60.15 = 5.685 + 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: - ((close_value/open_value) - 1) * leverage + 1x, 3x : ((close_value/open_value) - 1) * leverage + -1x,-3x: (1 - (close_value/open_value)) * leverage 2.1 quote - (62.8425 / 60.15) - 1 = 0.04476309226932673 + 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 - (56.8575 / 60.15) - 1 = -0.05473815461346632 + 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 - (65.835 / 60.15) - 1 = 0.0945137157107232 - fee: 0.003 + 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 + 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 + 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 @@ -403,13 +1011,17 @@ def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee): 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_usdt) # Buy @ 2.0 + # 1x Leverage, long # Custom closing rate and regular fee rate # Higher than open rate - 2.1 quote assert trade.calc_profit(rate=2.1) == 2.6925 @@ -429,6 +1041,70 @@ def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee): # Test with a custom fee rate on the close trade 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.is_short = True + trade.recalc_open_trade_value() + # 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_usdt, limit_sell_order_usdt, fee): @@ -437,6 +1113,9 @@ def test_calc_profit_ratio(limit_buy_order_usdt, limit_sell_order_usdt, fee): 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' @@ -444,6 +1123,8 @@ def test_calc_profit_ratio(limit_buy_order_usdt, limit_sell_order_usdt, fee): trade.open_order_id = 'something' trade.update(limit_buy_order_usdt) # Buy @ 2.0 + # 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 @@ -464,6 +1145,68 @@ def test_calc_profit_ratio(limit_buy_order_usdt, limit_sell_order_usdt, fee): 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.is_short = True + trade.recalc_open_trade_value() + # 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") @@ -788,6 +1531,60 @@ def test_adjust_stop_loss(fee): assert trade.stop_loss_pct == -0.1 +def test_adjust_stop_loss_short(fee): + trade = Trade( + pair='ADA/USDT', + 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_isolated_liq(0.63) + trade.adjust_stop_loss(0.59, -0.1) + assert trade.stop_loss == 0.63 + assert trade.isolated_liq == 0.63 + + def test_adjust_min_max_rates(fee): trade = Trade( pair='ADA/USDT', @@ -831,12 +1628,24 @@ 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): # Simulate dry_run entries trade = Trade( - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=0.001, amount=123.0, amount_requested=123.0, @@ -852,7 +1661,7 @@ def test_to_json(default_conf, fee): assert isinstance(result, dict) assert result == {'trade_id': None, - 'pair': 'ETH/BTC', + 'pair': 'ADA/USDT', 'is_open': None, 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), 'open_timestamp': int(trade.open_date.timestamp() * 1000), @@ -898,6 +1707,10 @@ def test_to_json(default_conf, fee): 'buy_tag': None, 'timeframe': None, 'exchange': 'binance', + 'leverage': None, + 'interest_rate': None, + 'isolated_liq': None, + 'is_short': None, } # Simulate dry_run entries @@ -965,6 +1778,10 @@ def test_to_json(default_conf, fee): 'buy_tag': 'buys_signal_001', 'timeframe': None, 'exchange': 'binance', + 'leverage': None, + 'interest_rate': None, + 'isolated_liq': None, + 'is_short': None, } @@ -1028,6 +1845,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='ADA/USDT', + 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_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 + + def test_update_fee(fee): trade = Trade( pair='ADA/USDT', @@ -1185,19 +2062,32 @@ 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) - o = Order.parse_from_ccxt_object({'id': '1234'}, 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object({'id': '1234'}, 'ADA/USDT', 'buy') assert isinstance(o, Order) - assert o.ft_pair == 'ETH/BTC' + assert o.ft_pair == 'ADA/USDT' 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', + 'symbol': 'ADA/USDT', 'type': 'limit', 'price': 1234.5, 'amount': 20.0, @@ -1206,9 +2096,9 @@ def test_update_order_from_ccxt(caplog): 'status': 'open', 'timestamp': 1599394315123 } - o = Order.parse_from_ccxt_object(ccxt_order, 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(ccxt_order, 'ADA/USDT', 'buy') assert isinstance(o, Order) - assert o.ft_pair == 'ETH/BTC' + assert o.ft_pair == 'ADA/USDT' assert o.ft_order_side == 'buy' assert o.order_id == '1234' assert o.order_type == 'limit' @@ -1303,7 +2193,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',