Merge pull request #5377 from samgermain/funding-fee

Funding Fee (Futures)
This commit is contained in:
Matthias 2021-10-13 19:04:14 +02:00 committed by GitHub
commit aed138ba03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 568 additions and 148 deletions

View File

@ -29,7 +29,7 @@ dependencies:
- colorama
- questionary
- prompt-toolkit
- schedule
# ============================
# 2/4 req dev

View File

@ -1,6 +1,6 @@
""" Bibox exchange subclass """
import logging
from typing import Dict
from typing import Dict, List
from freqtrade.exchange import Exchange
@ -24,3 +24,5 @@ class Bibox(Exchange):
def _ccxt_config(self) -> Dict:
# Parameters to add directly to ccxt sync/async initialization.
return {"has": {"fetchCurrencies": False}}
funding_fee_times: List[int] = [0, 8, 16] # hours of the day

View File

@ -28,6 +28,8 @@ class Binance(Exchange):
"trades_pagination_arg": "fromId",
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
}
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
# but the schedule won't check within this timeframe
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list
@ -183,7 +185,7 @@ class Binance(Exchange):
max_lev = 1/margin_req
return max_lev
@ retrier
@retrier
def _set_leverage(
self,
leverage: float,

View File

@ -1,7 +1,8 @@
""" Bybit exchange subclass """
import logging
from typing import Dict
from typing import Dict, List, Tuple
from freqtrade.enums import Collateral, TradingMode
from freqtrade.exchange import Exchange
@ -21,3 +22,11 @@ class Bybit(Exchange):
_ft_has: Dict = {
"ohlcv_candle_limit": 200,
}
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported
# (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported
]

View File

@ -9,7 +9,7 @@ import logging
from copy import deepcopy
from datetime import datetime, timezone
from math import ceil
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple, Union
import arrow
import ccxt
@ -72,6 +72,10 @@ class Exchange:
}
_ft_has: Dict = {}
# funding_fee_times is currently unused, but should ideally be used to properly
# schedule refresh times
funding_fee_times: List[int] = [] # hours of the day
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list
]
@ -207,7 +211,6 @@ class Exchange:
'secret': exchange_config.get('secret'),
'password': exchange_config.get('password'),
'uid': exchange_config.get('uid', ''),
# 'options': exchange_config.get('options', {})
}
if ccxt_kwargs:
logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
@ -1595,6 +1598,37 @@ class Exchange:
self._async_get_trade_history(pair=pair, since=since,
until=until, from_id=from_id))
@retrier
def get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float:
"""
Returns the sum of all funding fees that were exchanged for a pair within a timeframe
:param pair: (e.g. ADA/USDT)
:param since: The earliest time of consideration for calculating funding fees,
in unix time or as a datetime
"""
# TODO-lev: Add dry-run handling for this.
if not self.exchange_has("fetchFundingHistory"):
raise OperationalException(
f"fetch_funding_history() has not been implemented on ccxt.{self.name}")
if type(since) is datetime:
since = int(since.timestamp()) * 1000 # * 1000 for ms
try:
funding_history = self._api.fetch_funding_history(
pair=pair,
since=since
)
return sum(fee['amount'] for fee in funding_history)
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get funding fees due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
def fill_leverage_brackets(self):
"""
# TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken
@ -1622,8 +1656,6 @@ class Exchange:
Set's the leverage before making a trade, in order to not
have the same leverage on every trade
"""
# TODO-lev: Make a documentation page that says you can't run 2 bots
# TODO-lev: on the same account with leverage
if self._config['dry_run'] or not self.exchange_has("setLeverage"):
# Some exchanges only support one collateral type
return

View File

@ -21,6 +21,7 @@ class Ftx(Exchange):
"stoploss_on_exchange": True,
"ohlcv_candle_limit": 1500,
}
funding_fee_times: List[int] = list(range(0, 24))
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list

View File

@ -1,6 +1,6 @@
""" Gate.io exchange subclass """
import logging
from typing import Dict
from typing import Dict, List
from freqtrade.exchange import Exchange
@ -23,3 +23,5 @@ class Gateio(Exchange):
}
_headers = {'X-Gate-Channel-Id': 'freqtrade'}
funding_fee_times: List[int] = [0, 8, 16] # hours of the day

View File

@ -1,5 +1,5 @@
import logging
from typing import Dict
from typing import Dict, List
from freqtrade.exchange import Exchange
@ -21,3 +21,5 @@ class Hitbtc(Exchange):
"ohlcv_candle_limit": 1000,
"ohlcv_params": {"sort": "DESC"}
}
funding_fee_times: List[int] = [0, 8, 16] # hours of the day

View File

@ -23,6 +23,7 @@ class Kraken(Exchange):
"trades_pagination": "id",
"trades_pagination_arg": "since",
}
funding_fee_times: List[int] = [0, 4, 8, 12, 16, 20] # hours of the day
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list

View File

@ -1,6 +1,6 @@
""" Kucoin exchange subclass """
import logging
from typing import Dict
from typing import Dict, List
from freqtrade.exchange import Exchange
@ -24,3 +24,5 @@ class Kucoin(Exchange):
"order_time_in_force": ['gtc', 'fok', 'ioc'],
"time_in_force_parameter": "timeInForce",
}
funding_fee_times: List[int] = [4, 12, 20] # hours of the day

View File

@ -4,19 +4,20 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
import copy
import logging
import traceback
from datetime import datetime, timezone
from datetime import datetime, time, timezone
from math import isclose
from threading import Lock
from typing import Any, Dict, List, Optional
import arrow
from schedule import Scheduler
from freqtrade import __version__, constants
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
@ -104,6 +105,25 @@ class FreqtradeBot(LoggingMixin):
self._exit_lock = Lock()
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
if 'trading_mode' in self.config:
self.trading_mode = TradingMode(self.config['trading_mode'])
else:
self.trading_mode = TradingMode.SPOT
self._schedule = Scheduler()
if self.trading_mode == TradingMode.FUTURES:
def update():
self.update_funding_fees()
self.wallets.update()
# TODO: This would be more efficient if scheduled in utc time, and performed at each
# TODO: funding interval, specified by funding_fee_times on the exchange classes
for time_slot in range(0, 24):
for minutes in [0, 15, 30, 45]:
t = str(time(time_slot, minutes, 2))
self._schedule.every().day.at(t).do(update)
def notify_status(self, msg: str) -> None:
"""
Public method for users of this class (worker, etc.) to send notifications
@ -183,7 +203,8 @@ class FreqtradeBot(LoggingMixin):
# Then looking for buy opportunities
if self.get_free_open_trades():
self.enter_positions()
if self.trading_mode == TradingMode.FUTURES:
self._schedule.run_pending()
Trade.commit()
def process_stopped(self) -> None:
@ -239,6 +260,15 @@ class FreqtradeBot(LoggingMixin):
open_trades = len(Trade.get_open_trades())
return max(0, self.config['max_open_trades'] - open_trades)
def update_funding_fees(self):
if self.trading_mode == TradingMode.FUTURES:
for trade in Trade.get_open_trades():
funding_fees = self.exchange.get_funding_fees_from_exchange(
trade.pair,
trade.open_date
)
trade.funding_fees = funding_fees
def startup_update_open_orders(self):
"""
Updates open orders based on order list kept in the database.
@ -261,6 +291,9 @@ class FreqtradeBot(LoggingMixin):
logger.warning(f"Error updating Order {order.order_id} due to {e}")
if self.trading_mode == TradingMode.FUTURES:
self._schedule.run_pending()
def update_closed_trades_without_assigned_fees(self):
"""
Update closed trades without close fees assigned.
@ -424,7 +457,7 @@ class FreqtradeBot(LoggingMixin):
# running get_signal on historical data fetched
(signal, enter_tag) = self.strategy.get_entry_signal(
pair, self.strategy.timeframe, analyzed_df
)
)
if signal:
stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
@ -526,7 +559,7 @@ class FreqtradeBot(LoggingMixin):
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
side='long'
):
):
logger.info(f"User requested abortion of buying {pair}")
return False
amount = self.exchange.amount_to_precision(pair, amount)
@ -571,6 +604,12 @@ class FreqtradeBot(LoggingMixin):
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
open_date = datetime.now(timezone.utc)
if self.trading_mode == TradingMode.FUTURES:
funding_fees = self.exchange.get_funding_fees_from_exchange(pair, open_date)
else:
funding_fees = 0.0
trade = Trade(
pair=pair,
stake_amount=stake_amount,
@ -581,13 +620,15 @@ class FreqtradeBot(LoggingMixin):
fee_close=fee,
open_rate=enter_limit_filled_price,
open_rate_requested=enter_limit_requested,
open_date=datetime.utcnow(),
open_date=open_date,
exchange=self.exchange.id,
open_order_id=order_id,
strategy=self.strategy.get_strategy_name(),
# TODO-lev: compatibility layer for buy_tag (!)
buy_tag=enter_tag,
timeframe=timeframe_to_minutes(self.config['timeframe'])
timeframe=timeframe_to_minutes(self.config['timeframe']),
trading_mode=self.trading_mode,
funding_fees=funding_fees
)
trade.orders.append(order_obj)

View File

@ -49,11 +49,20 @@ 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 Properties
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')
# Margin Properties
interest_rate = get_column_def(cols, 'interest_rate', '0.0')
# Futures properties
funding_fees = get_column_def(cols, 'funding_fees', '0.0')
# If ticker-interval existed use that, else null.
if has_column(cols, 'ticker_interval'):
timeframe = get_column_def(cols, 'timeframe', 'ticker_interval')
@ -91,7 +100,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
trading_mode, leverage, isolated_liq, is_short,
interest_rate, funding_fees
)
select id, lower(exchange), pair,
is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
@ -108,8 +118,9 @@ 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
{trading_mode} trading_mode, {leverage} leverage, {isolated_liq} isolated_liq,
{is_short} is_short, {interest_rate} interest_rate,
{funding_fees} funding_fees
from {table_back_name}
"""))
@ -169,7 +180,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, 'funding_fees'):
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

@ -6,7 +6,7 @@ from datetime import datetime, timedelta, 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, NON_OPEN_EXCHANGE_STATES
from freqtrade.enums import SellType
from freqtrade.enums import SellType, TradingMode
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.leverage import interest
from freqtrade.misc import safe_value_fallback
@ -265,14 +265,19 @@ class LocalTrade():
buy_tag: Optional[str] = None
timeframe: Optional[int] = None
trading_mode: TradingMode = TradingMode.SPOT
# Leverage trading properties
is_short: bool = False
isolated_liq: Optional[float] = None
is_short: bool = False
leverage: float = 1.0
# Margin trading properties
interest_rate: float = 0.0
# Futures properties
funding_fees: Optional[float] = None
@property
def has_no_leverage(self) -> bool:
"""Returns true if this is a non-leverage, non-short trade"""
@ -439,7 +444,8 @@ class LocalTrade():
'interest_rate': self.interest_rate,
'isolated_liq': self.isolated_liq,
'is_short': self.is_short,
'trading_mode': self.trading_mode,
'funding_fees': self.funding_fees,
'open_order_id': self.open_order_id,
}
@ -643,7 +649,7 @@ class LocalTrade():
zero = Decimal(0.0)
# If nothing was borrowed
if self.has_no_leverage:
if self.has_no_leverage or self.trading_mode != TradingMode.MARGIN:
return zero
open_date = self.open_date.replace(tzinfo=None)
@ -657,6 +663,17 @@ class LocalTrade():
return interest(exchange_name=self.exchange, 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:
@ -673,20 +690,32 @@ class LocalTrade():
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):
total_interest = self.calculate_interest(interest_rate)
if self.is_short:
amount = amount + total_interest
return float(self._calc_base_close(amount, rate, fee))
else:
# Currency already owned for longs, no need to purchase
return float(self._calc_base_close(amount, rate, fee) - total_interest)
elif (trading_mode == TradingMode.FUTURES):
funding_fees = self.funding_fees or 0.0
if self.is_short:
return float(self._calc_base_close(amount, rate, fee)) - funding_fees
else:
return float(self._calc_base_close(amount, rate, fee)) + funding_fees
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,
@ -894,6 +923,8 @@ class Trade(_DECL_BASE, LocalTrade):
buy_tag = Column(String(100), nullable=True)
timeframe = Column(Integer, nullable=True)
trading_mode = Column(Enum(TradingMode), nullable=True)
# Leverage trading properties
leverage = Column(Float, nullable=True, default=1.0)
is_short = Column(Boolean, nullable=False, default=False)
@ -902,6 +933,9 @@ class Trade(_DECL_BASE, LocalTrade):
# Margin Trading Properties
interest_rate = Column(Float, nullable=False, default=0.0)
# Futures properties
funding_fees = Column(Float, nullable=True, default=None)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.recalc_open_trade_value()

View File

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

View File

@ -11,7 +11,7 @@ hyperopt = [
'joblib',
'progressbar2',
'psutil',
]
]
develop = [
'coveralls',
@ -31,7 +31,7 @@ jupyter = [
'nbstripout',
'ipykernel',
'nbconvert',
]
]
all_extra = plot + develop + jupyter + hyperopt
@ -41,7 +41,7 @@ setup(
'pytest-asyncio',
'pytest-cov',
'pytest-mock',
],
],
install_requires=[
# from requirements.txt
'ccxt>=1.50.48',
@ -72,7 +72,8 @@ setup(
'fastapi',
'uvicorn',
'pyjwt',
'aiofiles'
'aiofiles',
'schedule'
],
extras_require={
'dev': all_extra,

View File

@ -3048,6 +3048,74 @@ def test_calculate_backoff(retrycount, max_retries, expected):
assert calculate_backoff(retrycount, max_retries) == expected
@pytest.mark.parametrize("exchange_name", ['binance', 'ftx'])
def test_get_funding_fees_from_exchange(default_conf, mocker, exchange_name):
api_mock = MagicMock()
api_mock.fetch_funding_history = MagicMock(return_value=[
{
'amount': 0.14542,
'code': 'USDT',
'datetime': '2021-09-01T08:00:01.000Z',
'id': '485478',
'info': {'asset': 'USDT',
'income': '0.14542',
'incomeType': 'FUNDING_FEE',
'info': 'FUNDING_FEE',
'symbol': 'XRPUSDT',
'time': '1630382001000',
'tradeId': '',
'tranId': '993203'},
'symbol': 'XRP/USDT',
'timestamp': 1630382001000
},
{
'amount': -0.14642,
'code': 'USDT',
'datetime': '2021-09-01T16:00:01.000Z',
'id': '485479',
'info': {'asset': 'USDT',
'income': '-0.14642',
'incomeType': 'FUNDING_FEE',
'info': 'FUNDING_FEE',
'symbol': 'XRPUSDT',
'time': '1630314001000',
'tradeId': '',
'tranId': '993204'},
'symbol': 'XRP/USDT',
'timestamp': 1630314001000
}
])
type(api_mock).has = PropertyMock(return_value={'fetchFundingHistory': True})
# mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ')
unix_time = int(date_time.timestamp())
expected_fees = -0.001 # 0.14542341 + -0.14642341
fees_from_datetime = exchange.get_funding_fees_from_exchange(
pair='XRP/USDT',
since=date_time
)
fees_from_unix_time = exchange.get_funding_fees_from_exchange(
pair='XRP/USDT',
since=unix_time
)
assert(isclose(expected_fees, fees_from_datetime))
assert(isclose(expected_fees, fees_from_unix_time))
ccxt_exceptionhandlers(
mocker,
default_conf,
api_mock,
exchange_name,
"get_funding_fees_from_exchange",
"fetch_funding_history",
pair="XRP/USDT",
since=unix_time
)
@pytest.mark.parametrize('exchange', ['binance', 'kraken', 'ftx'])
@pytest.mark.parametrize('stake_amount,leverage,min_stake_with_lev', [
(9.0, 3.0, 3.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
@ -112,6 +112,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'interest_rate': 0.0,
'isolated_liq': None,
'is_short': False,
'funding_fees': 0.0,
'trading_mode': TradingMode.SPOT
}
mocker.patch('freqtrade.exchange.Exchange.get_rate',
@ -183,6 +185,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'interest_rate': 0.0,
'isolated_liq': None,
'is_short': False,
'funding_fees': 0.0,
'trading_mode': TradingMode.SPOT
}

View File

@ -11,7 +11,7 @@ import arrow
import pytest
from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT
from freqtrade.enums import RPCMessageType, RunMode, SellType, State
from freqtrade.enums import RPCMessageType, RunMode, SellType, State, TradingMode
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, OperationalException, PricingError,
TemporaryError)
@ -4278,3 +4278,36 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None:
assert valid_price_at_min_alwd > custom_price_under_min_alwd
assert valid_price_at_min_alwd < proposed_price
@pytest.mark.parametrize('trading_mode,calls,t1,t2', [
(TradingMode.SPOT, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"),
(TradingMode.MARGIN, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"),
(TradingMode.FUTURES, 31, "2021-09-01 00:00:02", "2021-09-01 08:00:01"),
(TradingMode.FUTURES, 32, "2021-09-01 00:00:00", "2021-09-01 08:00:01"),
(TradingMode.FUTURES, 32, "2021-09-01 00:00:02", "2021-09-01 08:00:02"),
(TradingMode.FUTURES, 33, "2021-09-01 00:00:00", "2021-09-01 08:00:02"),
(TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:02"),
(TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:03"),
(TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:04"),
(TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:05"),
(TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:06"),
(TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:07"),
(TradingMode.FUTURES, 33, "2021-08-31 23:59:58", "2021-09-01 08:00:07"),
])
def test_update_funding_fees(mocker, default_conf, trading_mode, calls, time_machine,
t1, t2):
time_machine.move_to(f"{t1} +00:00")
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_funding_fees', return_value=True)
default_conf['trading_mode'] = trading_mode
default_conf['collateral'] = 'isolated'
freqtrade = get_patched_freqtradebot(mocker, default_conf)
time_machine.move_to(f"{t2} +00:00")
# Check schedule jobs in debugging with freqtrade._schedule.jobs
freqtrade._schedule.run_pending()
assert freqtrade.update_funding_fees.call_count == calls

View File

@ -11,12 +11,16 @@ import pytest
from sqlalchemy import create_engine, inspect, text
from freqtrade import constants
from freqtrade.enums import 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, get_sides,
log_has, log_has_re)
spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURES
def test_init_create_session(default_conf):
# Check if init create a session
init_db(default_conf['db_url'], default_conf['dry_run'])
@ -81,7 +85,8 @@ def test_enter_exit_side(fee, is_short):
fee_close=fee.return_value,
exchange='binance',
is_short=is_short,
leverage=2.0
leverage=2.0,
trading_mode=margin
)
assert trade.enter_side == enter_side
assert trade.exit_side == exit_side
@ -101,7 +106,8 @@ def test_set_stop_loss_isolated_liq(fee):
fee_close=fee.return_value,
exchange='binance',
is_short=False,
leverage=2.0
leverage=2.0,
trading_mode=margin
)
trade.set_isolated_liq(0.09)
assert trade.isolated_liq == 0.09
@ -168,32 +174,40 @@ def test_set_stop_loss_isolated_liq(fee):
assert trade.initial_stop_loss == 0.09
@pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest', [
("binance", False, 3, 10, 0.0005, round(0.0008333333333333334, 8)),
("binance", True, 3, 10, 0.0005, 0.000625),
("binance", False, 3, 295, 0.0005, round(0.004166666666666667, 8)),
("binance", True, 3, 295, 0.0005, round(0.0031249999999999997, 8)),
("binance", False, 3, 295, 0.00025, round(0.0020833333333333333, 8)),
("binance", True, 3, 295, 0.00025, round(0.0015624999999999999, 8)),
("binance", False, 5, 295, 0.0005, 0.005),
("binance", True, 5, 295, 0.0005, round(0.0031249999999999997, 8)),
("binance", False, 1, 295, 0.0005, 0.0),
("binance", True, 1, 295, 0.0005, 0.003125),
@pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest,trading_mode', [
("binance", False, 3, 10, 0.0005, round(0.0008333333333333334, 8), margin),
("binance", True, 3, 10, 0.0005, 0.000625, margin),
("binance", False, 3, 295, 0.0005, round(0.004166666666666667, 8), margin),
("binance", True, 3, 295, 0.0005, round(0.0031249999999999997, 8), margin),
("binance", False, 3, 295, 0.00025, round(0.0020833333333333333, 8), margin),
("binance", True, 3, 295, 0.00025, round(0.0015624999999999999, 8), margin),
("binance", False, 5, 295, 0.0005, 0.005, margin),
("binance", True, 5, 295, 0.0005, round(0.0031249999999999997, 8), margin),
("binance", False, 1, 295, 0.0005, 0.0, spot),
("binance", True, 1, 295, 0.0005, 0.003125, margin),
("kraken", False, 3, 10, 0.0005, 0.040),
("kraken", True, 3, 10, 0.0005, 0.030),
("kraken", False, 3, 295, 0.0005, 0.06),
("kraken", True, 3, 295, 0.0005, 0.045),
("kraken", False, 3, 295, 0.00025, 0.03),
("kraken", True, 3, 295, 0.00025, 0.0225),
("kraken", False, 5, 295, 0.0005, round(0.07200000000000001, 8)),
("kraken", True, 5, 295, 0.0005, 0.045),
("kraken", False, 1, 295, 0.0005, 0.0),
("kraken", True, 1, 295, 0.0005, 0.045),
("binance", False, 3, 10, 0.0005, 0.0, futures),
("binance", True, 3, 295, 0.0005, 0.0, futures),
("binance", False, 5, 295, 0.0005, 0.0, futures),
("binance", True, 5, 295, 0.0005, 0.0, futures),
("binance", False, 1, 295, 0.0005, 0.0, futures),
("binance", True, 1, 295, 0.0005, 0.0, futures),
("kraken", False, 3, 10, 0.0005, 0.040, margin),
("kraken", True, 3, 10, 0.0005, 0.030, margin),
("kraken", False, 3, 295, 0.0005, 0.06, margin),
("kraken", True, 3, 295, 0.0005, 0.045, margin),
("kraken", False, 3, 295, 0.00025, 0.03, margin),
("kraken", True, 3, 295, 0.00025, 0.0225, margin),
("kraken", False, 5, 295, 0.0005, round(0.07200000000000001, 8), margin),
("kraken", True, 5, 295, 0.0005, 0.045, margin),
("kraken", False, 1, 295, 0.0005, 0.0, spot),
("kraken", True, 1, 295, 0.0005, 0.045, margin),
])
@pytest.mark.usefixtures("init_persistence")
def test_interest(market_buy_order_usdt, fee, exchange, is_short, lev, minutes, rate, interest):
def test_interest(market_buy_order_usdt, fee, exchange, is_short, lev, minutes, rate, interest,
trading_mode):
"""
10min, 5hr limit trade on Binance/Kraken at 3x,5x leverage
fee: 0.25 % quote
@ -258,21 +272,22 @@ def test_interest(market_buy_order_usdt, fee, exchange, is_short, lev, minutes,
exchange=exchange,
leverage=lev,
interest_rate=rate,
is_short=is_short
is_short=is_short,
trading_mode=trading_mode
)
assert round(float(trade.calculate_interest()), 8) == interest
@pytest.mark.parametrize('is_short,lev,borrowed', [
(False, 1.0, 0.0),
(True, 1.0, 30.0),
(False, 3.0, 40.0),
(True, 3.0, 30.0),
@pytest.mark.parametrize('is_short,lev,borrowed,trading_mode', [
(False, 1.0, 0.0, spot),
(True, 1.0, 30.0, margin),
(False, 3.0, 40.0, margin),
(True, 3.0, 30.0, margin),
])
@pytest.mark.usefixtures("init_persistence")
def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee,
caplog, is_short, lev, borrowed):
caplog, is_short, lev, borrowed, trading_mode):
"""
10 minute limit trade on Binance/Kraken at 1x, 3x leverage
fee: 0.25% quote
@ -347,18 +362,19 @@ def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee,
fee_close=fee.return_value,
exchange='binance',
is_short=is_short,
leverage=lev
leverage=lev,
trading_mode=trading_mode
)
assert trade.borrowed == borrowed
@pytest.mark.parametrize('is_short,open_rate,close_rate,lev,profit', [
(False, 2.0, 2.2, 1.0, round(0.0945137157107232, 8)),
(True, 2.2, 2.0, 3.0, round(0.2589996297562085, 8))
@pytest.mark.parametrize('is_short,open_rate,close_rate,lev,profit,trading_mode', [
(False, 2.0, 2.2, 1.0, round(0.0945137157107232, 8), spot),
(True, 2.2, 2.0, 3.0, round(0.2589996297562085, 8), margin),
])
@pytest.mark.usefixtures("init_persistence")
def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_usdt,
is_short, open_rate, close_rate, lev, profit):
is_short, open_rate, close_rate, lev, profit, trading_mode):
"""
10 minute limit trade on Binance/Kraken at 1x, 3x leverage
fee: 0.25% quote
@ -445,7 +461,8 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_
exchange='binance',
is_short=is_short,
interest_rate=0.0005,
leverage=lev
leverage=lev,
trading_mode=trading_mode
)
assert trade.open_order_id is None
assert trade.close_profit is None
@ -491,6 +508,7 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
fee_close=fee.return_value,
open_date=arrow.utcnow().datetime,
exchange='binance',
trading_mode=margin
)
trade.open_order_id = 'something'
@ -518,20 +536,28 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
caplog)
@pytest.mark.parametrize('exchange,is_short,lev,open_value,close_value,profit,profit_ratio', [
("binance", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232),
("binance", True, 1, 59.850, 66.1663784375, -6.316378437500013, -0.1055368159983292),
("binance", False, 3, 60.15, 65.83416667, 5.684166670000003, 0.2834995845386534),
("binance", True, 3, 59.85, 66.1663784375, -6.316378437500013, -0.3166104479949876),
@pytest.mark.parametrize(
'exchange,is_short,lev,open_value,close_value,profit,profit_ratio,trading_mode,funding_fees', [
("binance", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232, spot, 0.0),
("binance", True, 1, 59.850, 66.1663784375, -6.3163784375, -0.105536815998329, margin, 0.0),
("binance", False, 3, 60.15, 65.83416667, 5.68416667, 0.2834995845386534, margin, 0.0),
("binance", True, 3, 59.85, 66.1663784375, -6.3163784375, -0.3166104479949876, margin, 0.0),
("kraken", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232),
("kraken", True, 1, 59.850, 66.231165, -6.381165, -0.106619298245614),
("kraken", False, 3, 60.15, 65.795, 5.645, 0.2815461346633419),
("kraken", True, 3, 59.850, 66.231165, -6.381165000000003, -0.319857894736842),
])
("kraken", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232, spot, 0.0),
("kraken", True, 1, 59.850, 66.231165, -6.381165, -0.106619298245614, margin, 0.0),
("kraken", False, 3, 60.15, 65.795, 5.645, 0.2815461346633419, margin, 0.0),
("kraken", True, 3, 59.850, 66.231165, -6.381165000000003, -0.319857894736842, margin, 0.0),
("binance", False, 1, 60.15, 66.835, 6.685, 0.11113881961762262, futures, 1.0),
("binance", True, 1, 59.85, 67.165, -7.315, -0.12222222222222223, futures, -1.0),
("binance", False, 3, 60.15, 64.835, 4.685, 0.23366583541147135, futures, -1.0),
("binance", True, 3, 59.85, 65.165, -5.315, -0.26641604010025066, futures, 1.0),
])
@pytest.mark.usefixtures("init_persistence")
def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee, exchange,
is_short, lev, open_value, close_value, profit, profit_ratio):
def test_calc_open_close_trade_price(
limit_buy_order_usdt, limit_sell_order_usdt, fee, exchange, is_short, lev,
open_value, close_value, profit, profit_ratio, trading_mode, funding_fees
):
trade: Trade = Trade(
pair='ADA/USDT',
stake_amount=60.0,
@ -543,7 +569,9 @@ def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt
fee_close=fee.return_value,
exchange=exchange,
is_short=is_short,
leverage=lev
leverage=lev,
trading_mode=trading_mode,
funding_fees=funding_fees
)
trade.open_order_id = f'something-{is_short}-{lev}-{exchange}'
@ -572,6 +600,7 @@ def test_trade_close(limit_buy_order_usdt, limit_sell_order_usdt, fee):
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10),
interest_rate=0.0005,
exchange='binance',
trading_mode=margin
)
assert trade.close_profit is None
assert trade.close_date is None
@ -600,6 +629,7 @@ def test_calc_close_trade_price_exception(limit_buy_order_usdt, fee):
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='binance',
trading_mode=margin
)
trade.open_order_id = 'something'
@ -617,6 +647,7 @@ def test_update_open_order(limit_buy_order_usdt):
fee_open=0.1,
fee_close=0.1,
exchange='binance',
trading_mode=margin
)
assert trade.open_order_id is None
@ -641,6 +672,7 @@ def test_update_invalid_order(limit_buy_order_usdt):
fee_open=0.1,
fee_close=0.1,
exchange='binance',
trading_mode=margin
)
limit_buy_order_usdt['type'] = 'invalid'
with pytest.raises(ValueError, match=r'Unknown order type'):
@ -648,6 +680,7 @@ def test_update_invalid_order(limit_buy_order_usdt):
@pytest.mark.parametrize('exchange', ['binance', 'kraken'])
@pytest.mark.parametrize('trading_mode', [spot, margin, futures])
@pytest.mark.parametrize('lev', [1, 3])
@pytest.mark.parametrize('is_short,fee_rate,result', [
(False, 0.003, 60.18),
@ -666,7 +699,8 @@ def test_calc_open_trade_value(
lev,
is_short,
fee_rate,
result
result,
trading_mode
):
# 10 minute limit trade on Binance/Kraken at 1x, 3x leverage
# fee: 0.25 %, 0.3% quote
@ -692,7 +726,8 @@ def test_calc_open_trade_value(
fee_close=fee_rate,
exchange=exchange,
leverage=lev,
is_short=is_short
is_short=is_short,
trading_mode=trading_mode
)
trade.open_order_id = 'open_trade'
@ -700,26 +735,37 @@ def test_calc_open_trade_value(
assert trade._calc_open_trade_value() == result
@pytest.mark.parametrize('exchange,is_short,lev,open_rate,close_rate,fee_rate,result', [
('binance', False, 1, 2.0, 2.5, 0.0025, 74.8125),
('binance', False, 1, 2.0, 2.5, 0.003, 74.775),
('binance', False, 1, 2.0, 2.2, 0.005, 65.67),
('binance', False, 3, 2.0, 2.5, 0.0025, 74.81166667),
('binance', False, 3, 2.0, 2.5, 0.003, 74.77416667),
('kraken', False, 3, 2.0, 2.5, 0.0025, 74.7725),
('kraken', False, 3, 2.0, 2.5, 0.003, 74.735),
('kraken', True, 3, 2.2, 2.5, 0.0025, 75.2626875),
('kraken', True, 3, 2.2, 2.5, 0.003, 75.300225),
('binance', True, 3, 2.2, 2.5, 0.0025, 75.18906641),
('binance', True, 3, 2.2, 2.5, 0.003, 75.22656719),
('binance', True, 1, 2.2, 2.5, 0.0025, 75.18906641),
('binance', True, 1, 2.2, 2.5, 0.003, 75.22656719),
('kraken', True, 1, 2.2, 2.5, 0.0025, 75.2626875),
('kraken', True, 1, 2.2, 2.5, 0.003, 75.300225),
])
@pytest.mark.parametrize(
'exchange,is_short,lev,open_rate,close_rate,fee_rate,result,trading_mode,funding_fees', [
('binance', False, 1, 2.0, 2.5, 0.0025, 74.8125, spot, 0),
('binance', False, 1, 2.0, 2.5, 0.003, 74.775, spot, 0),
('binance', False, 1, 2.0, 2.2, 0.005, 65.67, margin, 0),
('binance', False, 3, 2.0, 2.5, 0.0025, 74.81166667, margin, 0),
('binance', False, 3, 2.0, 2.5, 0.003, 74.77416667, margin, 0),
('binance', True, 3, 2.2, 2.5, 0.0025, 75.18906641, margin, 0),
('binance', True, 3, 2.2, 2.5, 0.003, 75.22656719, margin, 0),
('binance', True, 1, 2.2, 2.5, 0.0025, 75.18906641, margin, 0),
('binance', True, 1, 2.2, 2.5, 0.003, 75.22656719, margin, 0),
# Kraken
('kraken', False, 3, 2.0, 2.5, 0.0025, 74.7725, margin, 0),
('kraken', False, 3, 2.0, 2.5, 0.003, 74.735, margin, 0),
('kraken', True, 3, 2.2, 2.5, 0.0025, 75.2626875, margin, 0),
('kraken', True, 3, 2.2, 2.5, 0.003, 75.300225, margin, 0),
('kraken', True, 1, 2.2, 2.5, 0.0025, 75.2626875, margin, 0),
('kraken', True, 1, 2.2, 2.5, 0.003, 75.300225, margin, 0),
('binance', False, 1, 2.0, 2.5, 0.0025, 75.8125, futures, 1),
('binance', False, 3, 2.0, 2.5, 0.0025, 73.8125, futures, -1),
('binance', True, 3, 2.0, 2.5, 0.0025, 74.1875, futures, 1),
('binance', True, 1, 2.0, 2.5, 0.0025, 76.1875, futures, -1),
])
@pytest.mark.usefixtures("init_persistence")
def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, open_rate,
exchange, is_short, lev, close_rate, fee_rate, result):
def test_calc_close_trade_price(
limit_buy_order_usdt, limit_sell_order_usdt, open_rate, exchange, is_short,
lev, close_rate, fee_rate, result, trading_mode, funding_fees
):
trade = Trade(
pair='ADA/USDT',
stake_amount=60.0,
@ -731,47 +777,83 @@ def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, ope
exchange=exchange,
interest_rate=0.0005,
is_short=is_short,
leverage=lev
leverage=lev,
trading_mode=trading_mode,
funding_fees=funding_fees
)
trade.open_order_id = 'close_trade'
assert round(trade.calc_close_trade_value(rate=close_rate, fee=fee_rate), 8) == result
@pytest.mark.parametrize('exchange,is_short,lev,close_rate,fee_close,profit,profit_ratio', [
('binance', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673),
('binance', False, 3, 2.1, 0.0025, 2.69166667, 0.13424771421446402),
('binance', True, 1, 2.1, 0.0025, -3.308815781249997, -0.05528514254385963),
('binance', True, 3, 2.1, 0.0025, -3.308815781249997, -0.1658554276315789),
@pytest.mark.parametrize(
'exchange,is_short,lev,close_rate,fee_close,profit,profit_ratio,trading_mode,funding_fees', [
('binance', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673, spot, 0),
('binance', False, 3, 2.1, 0.0025, 2.69166667, 0.13424771421446402, margin, 0),
('binance', True, 1, 2.1, 0.0025, -3.308815781249997, -0.05528514254385963, margin, 0),
('binance', True, 3, 2.1, 0.0025, -3.308815781249997, -0.1658554276315789, margin, 0),
('binance', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632),
('binance', False, 3, 1.9, 0.0025, -3.29333333, -0.16425602643391513),
('binance', True, 1, 1.9, 0.0025, 2.7063095312499996, 0.045218204365079395),
('binance', True, 3, 1.9, 0.0025, 2.7063095312499996, 0.13565461309523819),
('binance', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632, margin, 0),
('binance', False, 3, 1.9, 0.0025, -3.29333333, -0.16425602643391513, margin, 0),
('binance', True, 1, 1.9, 0.0025, 2.7063095312499996, 0.045218204365079395, margin, 0),
('binance', True, 3, 1.9, 0.0025, 2.7063095312499996, 0.13565461309523819, margin, 0),
('binance', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232),
('binance', False, 3, 2.2, 0.0025, 5.68416667, 0.2834995845386534),
('binance', True, 1, 2.2, 0.0025, -6.316378437499999, -0.1055368159983292),
('binance', True, 3, 2.2, 0.0025, -6.316378437499999, -0.3166104479949876),
('binance', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232, margin, 0),
('binance', False, 3, 2.2, 0.0025, 5.68416667, 0.2834995845386534, margin, 0),
('binance', True, 1, 2.2, 0.0025, -6.316378437499999, -0.1055368159983292, margin, 0),
('binance', True, 3, 2.2, 0.0025, -6.316378437499999, -0.3166104479949876, margin, 0),
('kraken', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673),
('kraken', False, 3, 2.1, 0.0025, 2.6525, 0.13229426433915248),
('kraken', True, 1, 2.1, 0.0025, -3.3706575, -0.05631842105263152),
('kraken', True, 3, 2.1, 0.0025, -3.3706575, -0.16895526315789455),
# # Kraken
('kraken', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673, spot, 0),
('kraken', False, 3, 2.1, 0.0025, 2.6525, 0.13229426433915248, margin, 0),
('kraken', True, 1, 2.1, 0.0025, -3.3706575, -0.05631842105263152, margin, 0),
('kraken', True, 3, 2.1, 0.0025, -3.3706575, -0.16895526315789455, margin, 0),
('kraken', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632),
('kraken', False, 3, 1.9, 0.0025, -3.3325, -0.16620947630922667),
('kraken', True, 1, 1.9, 0.0025, 2.6503575, 0.04428333333333334),
('kraken', True, 3, 1.9, 0.0025, 2.6503575, 0.13285000000000002),
('kraken', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632, margin, 0),
('kraken', False, 3, 1.9, 0.0025, -3.3325, -0.16620947630922667, margin, 0),
('kraken', True, 1, 1.9, 0.0025, 2.6503575, 0.04428333333333334, margin, 0),
('kraken', True, 3, 1.9, 0.0025, 2.6503575, 0.13285000000000002, margin, 0),
('kraken', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232),
('kraken', False, 3, 2.2, 0.0025, 5.645, 0.2815461346633419),
('kraken', True, 1, 2.2, 0.0025, -6.381165, -0.106619298245614),
('kraken', True, 3, 2.2, 0.0025, -6.381165, -0.319857894736842),
('kraken', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232, margin, 0),
('kraken', False, 3, 2.2, 0.0025, 5.645, 0.2815461346633419, margin, 0),
('kraken', True, 1, 2.2, 0.0025, -6.381165, -0.106619298245614, margin, 0),
('kraken', True, 3, 2.2, 0.0025, -6.381165, -0.319857894736842, margin, 0),
('binance', False, 1, 2.1, 0.003, 2.6610000000000014, 0.04423940149625927),
('binance', False, 1, 1.9, 0.003, -3.320999999999998, -0.05521197007481293),
('binance', False, 1, 2.2, 0.003, 5.652000000000008, 0.09396508728179565),
])
('binance', False, 1, 2.1, 0.003, 2.6610000000000014, 0.04423940149625927, spot, 0),
('binance', False, 1, 1.9, 0.003, -3.320999999999998, -0.05521197007481293, spot, 0),
('binance', False, 1, 2.2, 0.003, 5.652000000000008, 0.09396508728179565, spot, 0),
# # FUTURES, funding_fee=1
('binance', False, 1, 2.1, 0.0025, 3.6925, 0.06138819617622615, futures, 1),
('binance', False, 3, 2.1, 0.0025, 3.6925, 0.18416458852867845, futures, 1),
('binance', True, 1, 2.1, 0.0025, -2.3074999999999974, -0.038554720133667564, futures, 1),
('binance', True, 3, 2.1, 0.0025, -2.3074999999999974, -0.11566416040100269, futures, 1),
('binance', False, 1, 1.9, 0.0025, -2.2925, -0.0381130507065669, futures, 1),
('binance', False, 3, 1.9, 0.0025, -2.2925, -0.1143391521197007, futures, 1),
('binance', True, 1, 1.9, 0.0025, 3.707500000000003, 0.06194653299916464, futures, 1),
('binance', True, 3, 1.9, 0.0025, 3.707500000000003, 0.18583959899749392, futures, 1),
('binance', False, 1, 2.2, 0.0025, 6.685, 0.11113881961762262, futures, 1),
('binance', False, 3, 2.2, 0.0025, 6.685, 0.33341645885286786, futures, 1),
('binance', True, 1, 2.2, 0.0025, -5.315000000000005, -0.08880534670008355, futures, 1),
('binance', True, 3, 2.2, 0.0025, -5.315000000000005, -0.26641604010025066, futures, 1),
# FUTURES, funding_fee=-1
('binance', False, 1, 2.1, 0.0025, 1.6925000000000026, 0.028137988362427313, futures, -1),
('binance', False, 3, 2.1, 0.0025, 1.6925000000000026, 0.08441396508728194, futures, -1),
('binance', True, 1, 2.1, 0.0025, -4.307499999999997, -0.07197159565580624, futures, -1),
('binance', True, 3, 2.1, 0.0025, -4.307499999999997, -0.21591478696741873, futures, -1),
('binance', False, 1, 1.9, 0.0025, -4.292499999999997, -0.07136325852036574, futures, -1),
('binance', False, 3, 1.9, 0.0025, -4.292499999999997, -0.2140897755610972, futures, -1),
('binance', True, 1, 1.9, 0.0025, 1.7075000000000031, 0.02852965747702596, futures, -1),
('binance', True, 3, 1.9, 0.0025, 1.7075000000000031, 0.08558897243107788, futures, -1),
('binance', False, 1, 2.2, 0.0025, 4.684999999999995, 0.07788861180382378, futures, -1),
('binance', False, 3, 2.2, 0.0025, 4.684999999999995, 0.23366583541147135, futures, -1),
('binance', True, 1, 2.2, 0.0025, -7.315000000000005, -0.12222222222222223, futures, -1),
('binance', True, 3, 2.2, 0.0025, -7.315000000000005, -0.3666666666666667, futures, -1),
])
@pytest.mark.usefixtures("init_persistence")
def test_calc_profit(
limit_buy_order_usdt,
@ -783,7 +865,9 @@ def test_calc_profit(
close_rate,
fee_close,
profit,
profit_ratio
profit_ratio,
trading_mode,
funding_fees
):
"""
10 minute limit trade on Binance/Kraken at 1x, 3x leverage
@ -802,6 +886,7 @@ def test_calc_profit(
1x,-1x: 60.0 quote
3x,-3x: 20.0 quote
hours: 1/6 (10 minutes)
funding_fees: 1
borrowed
1x: 0 quote
3x: 40 quote
@ -913,6 +998,87 @@ def test_calc_profit(
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
futures (live):
funding_fee: 1
close_value:
equations:
1x,3x: (amount * close_rate) - (amount * close_rate * fee) + funding_fees
-1x,-3x: (amount * close_rate) + (amount * close_rate * fee) - funding_fees
2.1 quote
1x,3x: (30.00 * 2.1) - (30.00 * 2.1 * 0.0025) + 1 = 63.8425
-1x,-3x: (30.00 * 2.1) + (30.00 * 2.1 * 0.0025) - 1 = 62.1575
1.9 quote
1x,3x: (30.00 * 1.9) - (30.00 * 1.9 * 0.0025) + 1 = 57.8575
-1x,-3x: (30.00 * 1.9) + (30.00 * 1.9 * 0.0025) - 1 = 56.1425
2.2 quote:
1x,3x: (30.00 * 2.20) - (30.00 * 2.20 * 0.0025) + 1 = 66.835
-1x,-3x: (30.00 * 2.20) + (30.00 * 2.20 * 0.0025) - 1 = 65.165
total_profit:
2.1 quote
1x,3x: 63.8425 - 60.15 = 3.6925
-1x,-3x: 59.850 - 62.1575 = -2.3074999999999974
1.9 quote
1x,3x: 57.8575 - 60.15 = -2.2925
-1x,-3x: 59.850 - 56.1425 = 3.707500000000003
2.2 quote:
1x,3x: 66.835 - 60.15 = 6.685
-1x,-3x: 59.850 - 65.165 = -5.315000000000005
total_profit_ratio:
2.1 quote
1x: (63.8425 / 60.15) - 1 = 0.06138819617622615
3x: ((63.8425 / 60.15) - 1)*3 = 0.18416458852867845
-1x: 1 - (62.1575 / 59.850) = -0.038554720133667564
-3x: (1 - (62.1575 / 59.850))*3 = -0.11566416040100269
1.9 quote
1x: (57.8575 / 60.15) - 1 = -0.0381130507065669
3x: ((57.8575 / 60.15) - 1)*3 = -0.1143391521197007
-1x: 1 - (56.1425 / 59.850) = 0.06194653299916464
-3x: (1 - (56.1425 / 59.850))*3 = 0.18583959899749392
2.2 quote
1x: (66.835 / 60.15) - 1 = 0.11113881961762262
3x: ((66.835 / 60.15) - 1)*3 = 0.33341645885286786
-1x: 1 - (65.165 / 59.850) = -0.08880534670008355
-3x: (1 - (65.165 / 59.850))*3 = -0.26641604010025066
funding_fee: -1
close_value:
equations:
(amount * close_rate) - (amount * close_rate * fee) + funding_fees
(amount * close_rate) - (amount * close_rate * fee) - funding_fees
2.1 quote
1x,3x: (30.00 * 2.1) - (30.00 * 2.1 * 0.0025) + (-1) = 61.8425
-1x,-3x: (30.00 * 2.1) + (30.00 * 2.1 * 0.0025) - (-1) = 64.1575
1.9 quote
1x,3x: (30.00 * 1.9) - (30.00 * 1.9 * 0.0025) + (-1) = 55.8575
-1x,-3x: (30.00 * 1.9) + (30.00 * 1.9 * 0.0025) - (-1) = 58.1425
2.2 quote:
1x,3x: (30.00 * 2.20) - (30.00 * 2.20 * 0.0025) + (-1) = 64.835
-1x,-3x: (30.00 * 2.20) + (30.00 * 2.20 * 0.0025) - (-1) = 67.165
total_profit:
2.1 quote
1x,3x: 61.8425 - 60.15 = 1.6925000000000026
-1x,-3x: 59.850 - 64.1575 = -4.307499999999997
1.9 quote
1x,3x: 55.8575 - 60.15 = -4.292499999999997
-1x,-3x: 59.850 - 58.1425 = 1.7075000000000031
2.2 quote:
1x,3x: 64.835 - 60.15 = 4.684999999999995
-1x,-3x: 59.850 - 67.165 = -7.315000000000005
total_profit_ratio:
2.1 quote
1x: (61.8425 / 60.15) - 1 = 0.028137988362427313
3x: ((61.8425 / 60.15) - 1)*3 = 0.08441396508728194
-1x: 1 - (64.1575 / 59.850) = -0.07197159565580624
-3x: (1 - (64.1575 / 59.850))*3 = -0.21591478696741873
1.9 quote
1x: (55.8575 / 60.15) - 1 = -0.07136325852036574
3x: ((55.8575 / 60.15) - 1)*3 = -0.2140897755610972
-1x: 1 - (58.1425 / 59.850) = 0.02852965747702596
-3x: (1 - (58.1425 / 59.850))*3 = 0.08558897243107788
2.2 quote
1x: (64.835 / 60.15) - 1 = 0.07788861180382378
3x: ((64.835 / 60.15) - 1)*3 = 0.23366583541147135
-1x: 1 - (67.165 / 59.850) = -0.12222222222222223
-3x: (1 - (67.165 / 59.850))*3 = -0.3666666666666667
"""
trade = Trade(
pair='ADA/USDT',
@ -925,7 +1091,9 @@ def test_calc_profit(
is_short=is_short,
leverage=lev,
fee_open=0.0025,
fee_close=fee_close
fee_close=fee_close,
trading_mode=trading_mode,
funding_fees=funding_fees
)
trade.open_order_id = 'something'
@ -1439,6 +1607,8 @@ def test_to_json(default_conf, fee):
'interest_rate': None,
'isolated_liq': None,
'is_short': None,
'trading_mode': None,
'funding_fees': None
}
# Simulate dry_run entries
@ -1510,6 +1680,8 @@ def test_to_json(default_conf, fee):
'interest_rate': None,
'isolated_liq': None,
'is_short': None,
'trading_mode': None,
'funding_fees': None
}