Created FundingFee class and added funding_fee to LocalTrade and freqtradebot

This commit is contained in:
Sam Germain 2021-08-03 12:55:22 -06:00
parent 797d7e5ce6
commit e3845ff808
12 changed files with 397 additions and 64 deletions

View File

@ -1,8 +1,11 @@
# flake8: noqa: F401
from freqtrade.enums.backteststate import BacktestState
from freqtrade.enums.collateral import Collateral
from freqtrade.enums.interestmode import InterestMode
from freqtrade.enums.liqformula import LiqFormula
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
from freqtrade.enums.signaltype import SignalTagType, SignalType
from freqtrade.enums.state import State
from freqtrade.enums.tradingmode import TradingMode

View File

@ -0,0 +1,11 @@
from enum import Enum
class Collateral(Enum):
"""
Enum to distinguish between
cross margin/futures collateral and
isolated margin/futures collateral
"""
CROSS = "cross"
ISOLATED = "isolated"

View File

@ -0,0 +1,108 @@
# from decimal import Decimal
from enum import Enum
from freqtrade.enums.collateral import Collateral
from freqtrade.enums.tradingmode import TradingMode
from freqtrade.exceptions import OperationalException
# from math import ceil
class LiqFormula(Enum):
"""Equations to calculate liquidation price"""
BINANCE = "Binance"
KRAKEN = "Kraken"
FTX = "FTX"
NONE = None
def __call__(self, **k):
trading_mode: TradingMode = k['trading_mode']
if trading_mode == TradingMode.SPOT or self.name == "NONE":
return None
collateral: Collateral = k['collateral']
if self.name == "BINANCE":
return binance(trading_mode, collateral)
elif self.name == "KRAKEN":
return kraken(trading_mode, collateral)
elif self.name == "FTX":
return ftx(trading_mode, collateral)
else:
exception(self.name, trading_mode, collateral)
def exception(name: str, trading_mode: TradingMode, collateral: Collateral):
"""
Raises an exception if exchange used doesn't support desired leverage mode
:param name: Name of the exchange
:param trading_mode: spot, margin, futures
:param collateral: cross, isolated
"""
raise OperationalException(
f"{name} does not support {collateral.value} {trading_mode.value} trading")
def binance(name: str, trading_mode: TradingMode, collateral: Collateral):
"""
Calculates the liquidation price on Binance
:param name: Name of the exchange
:param trading_mode: spot, margin, futures
:param collateral: cross, isolated
"""
# TODO-lev: Additional arguments, fill in formulas
if trading_mode == TradingMode.MARGIN and collateral == Collateral.CROSS:
# TODO-lev: perform a calculation based on this formula
# https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed
exception(name, trading_mode, collateral)
elif trading_mode == TradingMode.FUTURES and collateral == Collateral.CROSS:
# TODO-lev: perform a calculation based on this formula
# https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
exception(name, trading_mode, collateral)
elif trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED:
# TODO-lev: perform a calculation based on this formula
# https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
exception(name, trading_mode, collateral)
# If nothing was returned
exception(name, trading_mode, collateral)
def kraken(name: str, trading_mode: TradingMode, collateral: Collateral):
"""
Calculates the liquidation price on Kraken
:param name: Name of the exchange
:param trading_mode: spot, margin, futures
:param collateral: cross, isolated
"""
# TODO-lev: Additional arguments, fill in formulas
if collateral == Collateral.CROSS:
if trading_mode == TradingMode.MARGIN:
exception(name, trading_mode, collateral)
# TODO-lev: perform a calculation based on this formula
# https://support.kraken.com/hc/en-us/articles/203325763-Margin-Call-Level-and-Margin-Liquidation-Level
elif trading_mode == TradingMode.FUTURES:
exception(name, trading_mode, collateral)
# If nothing was returned
exception(name, trading_mode, collateral)
def ftx(name: str, trading_mode: TradingMode, collateral: Collateral):
"""
Calculates the liquidation price on FTX
:param name: Name of the exchange
:param trading_mode: spot, margin, futures
:param collateral: cross, isolated
"""
if collateral == Collateral.CROSS:
# TODO-lev: Additional arguments, fill in formulas
exception(name, trading_mode, collateral)
# If nothing was returned
exception(name, trading_mode, collateral)

View File

@ -0,0 +1,11 @@
from enum import Enum
class TradingMode(Enum):
"""
Enum to distinguish between
spot, margin, futures or any other trading method
"""
SPOT = "spot"
MARGIN = "margin"
FUTURES = "futures"

View File

@ -16,10 +16,11 @@ from freqtrade.configuration import validate_config_consistency
from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge
from freqtrade.enums import RPCMessageType, SellType, State
from freqtrade.enums import RPCMessageType, SellType, State, TradingMode
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.leverage import FundingFee
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
from freqtrade.mixins import LoggingMixin
from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db
@ -102,6 +103,11 @@ class FreqtradeBot(LoggingMixin):
self._sell_lock = Lock()
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
self.trading_mode = TradingMode.SPOT
if self.trading_mode == TradingMode.FUTURES:
self.funding_fee = FundingFee()
self.funding_fee.start()
def notify_status(self, msg: str) -> None:
"""
Public method for users of this class (worker, etc.) to send notifications
@ -553,6 +559,10 @@ class FreqtradeBot(LoggingMixin):
amount = safe_value_fallback(order, 'filled', 'amount')
buy_limit_filled_price = safe_value_fallback(order, 'average', 'price')
funding_fee = (self.funding_fee.initial_funding_fee(amount)
if self.trading_mode == TradingMode.FUTURES
else None)
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
trade = Trade(
@ -570,10 +580,15 @@ class FreqtradeBot(LoggingMixin):
open_order_id=order_id,
strategy=self.strategy.get_strategy_name(),
buy_tag=buy_tag,
timeframe=timeframe_to_minutes(self.config['timeframe'])
timeframe=timeframe_to_minutes(self.config['timeframe']),
funding_fee=funding_fee,
trading_mode=self.trading_mode
)
trade.orders.append(order_obj)
if self.trading_mode == TradingMode.FUTURES:
self.funding_fee.add_new_trade(trade)
# Update fees if order is closed
if order_status == 'closed':
self.update_trade_state(trade, order_id, order)

View File

@ -0,0 +1,2 @@
# flake8: noqa: F401
from freqtrade.leverage.funding_fee import FundingFee

View File

@ -0,0 +1,71 @@
from datetime import datetime, timedelta
from typing import List
import schedule
from freqtrade.persistence import Trade
class FundingFee:
trades: List[Trade]
begin_times = [
# TODO-lev: Make these UTC time
"23:59:45",
"07:59:45",
"15:59:45",
]
def _is_time_between(self, begin_time, end_time):
# If check time is not given, default to current UTC time
check_time = datetime.utcnow().time()
if begin_time < end_time:
return check_time >= begin_time and check_time <= end_time
else: # crosses midnight
return check_time >= begin_time or check_time <= end_time
def _apply_funding_fees(self, num_of: int = 1):
if num_of == 0:
return
for trade in self.trades:
trade.adjust_funding_fee(self._calculate(trade.amount) * num_of)
def _calculate(self, amount):
# TODO-futures: implement
# TODO-futures: Check how other exchages do it and adjust accordingly
# https://www.binance.com/en/support/faq/360033525031
# mark_price =
# contract_size = maybe trade.amount
# funding_rate = # https://www.binance.com/en/futures/funding-history/0
# nominal_value = mark_price * contract_size
# adjustment = nominal_value * funding_rate
# return adjustment
return
def initial_funding_fee(self, amount) -> float:
# A funding fee interval is applied immediately if within 30s of an iterval
for begin_string in self.begin_times:
begin_time = datetime.strptime(begin_string, "%H:%M:%S")
end_time = (begin_time + timedelta(seconds=30))
if self._is_time_between(begin_time.time(), end_time.time()):
return self._calculate(amount)
return 0.0
def start(self):
for interval in self.begin_times:
schedule.every().day.at(interval).do(self._apply_funding_fees())
# https://stackoverflow.com/a/30393162/6331353
# TODO-futures: Put schedule.run_pending() somewhere in the bot_loop
def reboot(self):
# TODO-futures Find out how many begin_times have passed since last funding_fee added
amount_missed = 0
self.apply_funding_fees(num_of=amount_missed)
self.start()
def add_new_trade(self, trade):
self.trades.append(trade)
def remove_trade(self, trade):
self.trades.remove(trade)

View File

@ -49,12 +49,18 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
strategy = get_column_def(cols, 'strategy', 'null')
buy_tag = get_column_def(cols, 'buy_tag', 'null')
trading_mode = get_column_def(cols, 'trading_mode', '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')
liq_formula = get_column_def(cols, 'liq_formula', 'null')
# sqlite does not support literals for booleans
interest_mode = get_column_def(cols, 'interest_mode', 'null')
interest_rate = get_column_def(cols, 'interest_rate', '0.0')
funding_fee = get_column_def(cols, 'funding_fee', '0.0')
last_funding_adjustment = get_column_def(cols, 'last_funding_adjustment', 'null')
# If ticker-interval existed use that, else null.
if has_column(cols, 'ticker_interval'):
timeframe = get_column_def(cols, 'timeframe', 'ticker_interval')
@ -66,7 +72,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
# TODO-lev: 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')
@ -92,7 +98,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, buy_tag,
timeframe, open_trade_value, close_profit_abs,
leverage, interest_rate, isolated_liq, is_short, interest_mode
trading_mode, leverage, isolated_liq, is_short, liq_formula, interest_mode,
interest_rate, funding_fee, last_funding_adjustment
)
select id, lower(exchange), pair,
is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
@ -109,9 +116,10 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
{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,
{leverage} leverage, {interest_rate} interest_rate,
{isolated_liq} isolated_liq, {is_short} is_short,
{interest_mode} interest_mode
{trading_mode} trading_mode, {leverage} leverage, {isolated_liq} isolated_liq,
{is_short} is_short, {liq_formula} liq_formula, {interest_mode} interest_mode,
{interest_rate} interest_rate, {funding_fee} funding_fee,
{last_funding_adjustment} last_funding_adjustment
from {table_back_name}
"""))
@ -171,7 +179,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, 'is_short'):
if not has_column(cols, 'buy_tag'):
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!

View File

@ -2,7 +2,7 @@
This module contains the class to persist trades into SQLite
"""
import logging
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from typing import Any, Dict, List, Optional
@ -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 InterestMode, SellType
from freqtrade.enums import InterestMode, LiqFormula, SellType, TradingMode
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.misc import safe_value_fallback
from freqtrade.persistence.migrations import check_migrate
@ -92,6 +92,12 @@ def clean_dry_run_db() -> None:
Trade.commit()
def hour_rounder(t):
# Rounds to nearest hour by adding a timedelta hour if minute >= 30
return (
t.replace(second=0, microsecond=0, minute=0, hour=t.hour) + timedelta(hours=t.minute//30))
class Order(_DECL_BASE):
"""
Order database model
@ -264,12 +270,21 @@ class LocalTrade():
buy_tag: Optional[str] = None
timeframe: Optional[int] = None
# Margin trading properties
interest_rate: float = 0.0
trading_mode: TradingMode = TradingMode.SPOT
# Leverage trading properties
isolated_liq: Optional[float] = None
is_short: bool = False
leverage: float = 1.0
liq_formula: LiqFormula = LiqFormula.NONE
# Margin trading properties
interest_mode: InterestMode = InterestMode.NONE
interest_rate: float = 0.0
# Futures properties
funding_fee: Optional[float] = None
last_funding_adjustment: Optional[datetime] = None
@property
def has_no_leverage(self) -> bool:
@ -316,7 +331,7 @@ class LocalTrade():
for key in kwargs:
setattr(self, key, kwargs[key])
if self.isolated_liq:
self.set_isolated_liq(self.isolated_liq)
self.set_isolated_liq(isolated_liq=self.isolated_liq)
self.recalc_open_trade_value()
def _set_stop_loss(self, stop_loss: float, percent: float):
@ -342,11 +357,17 @@ class LocalTrade():
self.stop_loss_pct = -1 * abs(percent)
self.stoploss_last_update = datetime.utcnow()
def set_isolated_liq(self, isolated_liq: float):
def set_isolated_liq(self, **k):
"""
Method you should use to set self.liquidation price.
Assures stop_loss is not passed the liquidation price
"""
if k['isolated_liq']:
isolated_liq = k['isolated_liq']
else:
isolated_liq: float == self.liq_formula(trading_mode=self.trading_mode, **k)
if self.stop_loss is not None:
if self.is_short:
self.stop_loss = min(self.stop_loss, isolated_liq)
@ -432,11 +453,16 @@ 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,
"trading_mode": self.trading_mode,
"isolated_liq": self.isolated_liq,
"is_short": self.is_short,
"leverage": self.leverage,
"liq_formula": self.liq_formula,
"interest_mode": self.interest_mode,
"interest_rate": self.interest_rate,
"funding_fee": self.funding_fee,
"last_funding_adjustment": (self.last_funding_adjustment.strftime(DATETIME_PRINT_FORMAT)
if self.last_funding_adjustment else None),
'open_order_id': self.open_order_id,
}
@ -513,6 +539,10 @@ class LocalTrade():
f"Trailing stoploss saved us: "
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.")
def adjust_funding_fee(self, adjustment):
self.funding_fee = self.funding_fee + adjustment
self.last_funding_adjustment = datetime.utcnow()
def update(self, order: Dict) -> None:
"""
Updates this entity with amount and actual open/close rates.
@ -545,10 +575,11 @@ class LocalTrade():
elif order_type in ('market', 'limit') and self.exit_side == order['side']:
if self.is_open:
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
# TODO-lev: On shorts, you buy a little bit more than the amount (amount + interest)
# TODO-lev: 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
# TODO-lev: Double check this
self.close(safe_value_fallback(order, 'average', 'price'))
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
@ -650,8 +681,20 @@ class LocalTrade():
rate = Decimal(interest_rate or self.interest_rate)
borrowed = Decimal(self.borrowed)
# TODO-lev: Pass trading mode to interest_mode maybe
return self.interest_mode(borrowed=borrowed, rate=rate, hours=hours)
def _calc_base_close(self, amount: Decimal, rate: Optional[float] = None,
fee: Optional[float] = None) -> Decimal:
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
fees = close_trade * Decimal(fee or self.fee_close)
if self.is_short:
return close_trade + fees
else:
return close_trade - fees
def calc_close_trade_value(self, rate: Optional[float] = None,
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
@ -665,23 +708,36 @@ class LocalTrade():
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_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)
amount = Decimal(self.amount)
trading_mode = self.trading_mode or TradingMode.SPOT
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
fees = close_trade * Decimal(fee or self.fee_close)
if trading_mode == TradingMode.SPOT:
return float(self._calc_base_close(amount, rate, fee))
if self.is_short:
return float(close_trade + fees)
elif (trading_mode == TradingMode.MARGIN):
interest = self.calculate_interest(interest_rate)
if self.is_short:
amount = amount + interest
return float(self._calc_base_close(amount, rate, fee))
else:
# Currency already owned for longs, no need to purchase
return float(self._calc_base_close(amount, rate, fee) - interest)
elif (trading_mode == TradingMode.FUTURES):
funding_fee = self.funding_fee or 0.0
if self.is_short:
return float(self._calc_base_close(amount, rate, fee)) + funding_fee
else:
return float(self._calc_base_close(amount, rate, fee)) - funding_fee
else:
return float(close_trade - fees - interest)
raise OperationalException(
f"{self.trading_mode.value} trading is not yet available using freqtrade")
def calc_profit(self, rate: Optional[float] = None,
fee: Optional[float] = None,
@ -883,19 +939,27 @@ 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-mg: Change to close_reason
sell_order_status = Column(String(100), nullable=True) # TODO-mg: Change to close_order_status
sell_reason = Column(String(100), nullable=True) # TODO-lev: Change to close_reason
sell_order_status = Column(String(100), nullable=True) # TODO-lev: Change to close_order_status
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)
trading_mode = Column(Enum(TradingMode))
# Leverage trading properties
isolated_liq = Column(Float, nullable=True)
is_short = Column(Boolean, nullable=False, default=False)
leverage = Column(Float, nullable=True, default=1.0)
liq_formula = Column(Enum(LiqFormula), nullable=True)
# Margin trading properties
interest_mode = Column(Enum(InterestMode), nullable=True)
# End of margin trading properties
interest_rate = Column(Float, nullable=False, default=0.0)
# Futures properties
funding_fee = Column(Float, nullable=True, default=None)
last_funding_adjustment = Column(DateTime, nullable=True)
def __init__(self, **kwargs):
super().__init__(**kwargs)

View File

@ -41,3 +41,6 @@ colorama==0.4.4
# Building config files interactively
questionary==1.10.0
prompt-toolkit==3.0.19
#Futures
schedule==1.1.0

View File

@ -8,7 +8,7 @@ import pytest
from numpy import isnan
from freqtrade.edge import PairInfo
from freqtrade.enums import State
from freqtrade.enums import State, TradingMode
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
from freqtrade.persistence import Trade
from freqtrade.persistence.pairlock_middleware import PairLocks
@ -108,10 +108,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,
'interest_rate': 0.0,
'trading_mode': TradingMode.SPOT,
'isolated_liq': None,
'is_short': False,
'leverage': 1.0,
'liq_formula': None,
'interest_mode': None,
'interest_rate': 0.0,
'funding_fee': None,
'last_funding_adjustment': None,
}
mocker.patch('freqtrade.exchange.Exchange.get_rate',
@ -179,10 +184,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,
'interest_rate': 0.0,
'trading_mode': TradingMode.SPOT,
'isolated_liq': None,
'is_short': False,
'leverage': 1.0,
'liq_formula': None,
'interest_mode': None,
'interest_rate': 0.0,
'funding_fee': None,
'last_funding_adjustment': None,
}

View File

@ -11,7 +11,7 @@ import pytest
from sqlalchemy import create_engine, inspect, text
from freqtrade import constants
from freqtrade.enums import InterestMode
from freqtrade.enums import InterestMode, TradingMode
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, create_mock_trades_with_leverage, log_has, log_has_re
@ -91,7 +91,7 @@ def test_enter_exit_side(fee):
@pytest.mark.usefixtures("init_persistence")
def test__set_stop_loss_isolated_liq(fee):
def test_set_stop_loss_isolated_liq(fee):
trade = Trade(
id=2,
pair='ADA/USDT',
@ -106,7 +106,7 @@ def test__set_stop_loss_isolated_liq(fee):
is_short=False,
leverage=2.0
)
trade.set_isolated_liq(0.09)
trade.set_isolated_liq(isolated_liq=0.09)
assert trade.isolated_liq == 0.09
assert trade.stop_loss == 0.09
assert trade.initial_stop_loss == 0.09
@ -116,12 +116,12 @@ def test__set_stop_loss_isolated_liq(fee):
assert trade.stop_loss == 0.1
assert trade.initial_stop_loss == 0.09
trade.set_isolated_liq(0.08)
trade.set_isolated_liq(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)
trade.set_isolated_liq(isolated_liq=0.11)
assert trade.isolated_liq == 0.11
assert trade.stop_loss == 0.11
assert trade.initial_stop_loss == 0.09
@ -145,7 +145,7 @@ def test__set_stop_loss_isolated_liq(fee):
trade.stop_loss = None
trade.initial_stop_loss = None
trade.set_isolated_liq(0.09)
trade.set_isolated_liq(isolated_liq=0.09)
assert trade.isolated_liq == 0.09
assert trade.stop_loss == 0.09
assert trade.initial_stop_loss == 0.09
@ -155,12 +155,12 @@ def test__set_stop_loss_isolated_liq(fee):
assert trade.stop_loss == 0.08
assert trade.initial_stop_loss == 0.09
trade.set_isolated_liq(0.1)
trade.set_isolated_liq(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)
trade.set_isolated_liq(isolated_liq=0.07)
assert trade.isolated_liq == 0.07
assert trade.stop_loss == 0.07
assert trade.initial_stop_loss == 0.09
@ -237,7 +237,8 @@ def test_interest(market_buy_order_usdt, fee):
exchange='kraken',
leverage=3.0,
interest_rate=0.0005,
interest_mode=InterestMode.HOURSPERDAY
interest_mode=InterestMode.HOURSPERDAY,
trading_mode=TradingMode.MARGIN
)
# 10min, 3x leverage
@ -506,7 +507,7 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca
open_date=arrow.utcnow().datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='binance',
exchange='binance'
)
assert trade.open_order_id is None
assert trade.close_profit is None
@ -550,7 +551,8 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca
is_short=True,
leverage=3.0,
interest_rate=0.0005,
interest_mode=InterestMode.HOURSPERDAY
interest_mode=InterestMode.HOURSPERDAY,
trading_mode=TradingMode.MARGIN
)
trade.open_order_id = 'something'
trade.update(limit_sell_order_usdt)
@ -642,7 +644,9 @@ def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt
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.trading_mode = TradingMode.MARGIN
trade.leverage = 3
trade.interest_mode = InterestMode.HOURSPERDAY
assert trade._calc_open_trade_value() == 60.15
@ -801,12 +805,19 @@ def test_calc_open_trade_value(limit_buy_order_usdt, fee):
# Get the open rate price with the standard fee rate
assert trade._calc_open_trade_value() == 60.15
# Margin
trade.trading_mode = TradingMode.MARGIN
trade.is_short = True
trade.recalc_open_trade_value()
assert trade._calc_open_trade_value() == 59.85
# 3x short margin leverage
trade.leverage = 3
trade.interest_mode = InterestMode.HOURSPERDAY
assert trade._calc_open_trade_value() == 59.85
# 3x long margin leverage
trade.is_short = False
trade.recalc_open_trade_value()
assert trade._calc_open_trade_value() == 60.15
@ -844,6 +855,7 @@ def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee
assert trade.calc_close_trade_value(fee=0.005) == 65.67
# 3x leverage binance
trade.trading_mode = TradingMode.MARGIN
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
@ -1044,6 +1056,8 @@ def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee):
trade.open_trade_value = 0.0
trade.open_trade_value = trade._calc_open_trade_value()
# Margin
trade.trading_mode = TradingMode.MARGIN
# 3x leverage, long ###################################################
trade.leverage = 3.0
# Higher than open rate - 2.1 quote
@ -1147,6 +1161,8 @@ def test_calc_profit_ratio(limit_buy_order_usdt, limit_sell_order_usdt, fee):
assert trade.calc_profit_ratio(fee=0.003) == 0.0
trade.open_trade_value = trade._calc_open_trade_value()
# Margin
trade.trading_mode = TradingMode.MARGIN
# 3x leverage, long ###################################################
trade.leverage = 3.0
# 2.1 quote - Higher than open rate
@ -1575,11 +1591,11 @@ def test_adjust_stop_loss_short(fee):
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 round(trade.stop_loss, 8) == 0.66
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.set_isolated_liq(isolated_liq=0.63)
trade.adjust_stop_loss(0.59, -0.1)
assert trade.stop_loss == 0.63
assert trade.isolated_liq == 0.63
@ -1707,10 +1723,15 @@ def test_to_json(default_conf, fee):
'buy_tag': None,
'timeframe': None,
'exchange': 'binance',
'leverage': None,
'interest_rate': None,
'trading_mode': None,
'isolated_liq': None,
'is_short': None,
'leverage': None,
'liq_formula': None,
'interest_mode': None,
'interest_rate': None,
'funding_fee': None,
'last_funding_adjustment': None
}
# Simulate dry_run entries
@ -1778,10 +1799,15 @@ def test_to_json(default_conf, fee):
'buy_tag': 'buys_signal_001',
'timeframe': None,
'exchange': 'binance',
'leverage': None,
'interest_rate': None,
'trading_mode': None,
'isolated_liq': None,
'is_short': None,
'leverage': None,
'liq_formula': None,
'interest_mode': None,
'interest_rate': None,
'funding_fee': None,
'last_funding_adjustment': None
}
@ -1899,7 +1925,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_isolated_liq(1.0)
trade_adj.set_isolated_liq(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
@ -2197,6 +2223,7 @@ def test_Trade_object_idem():
'get_open_trades_without_assigned_fees',
'get_open_order_trades',
'get_trades',
'last_funding_adjustment'
)
# Parent (LocalTrade) should have the same attributes