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 - colorama
- questionary - questionary
- prompt-toolkit - prompt-toolkit
- schedule
# ============================ # ============================
# 2/4 req dev # 2/4 req dev

View File

@ -1,6 +1,6 @@
""" Bibox exchange subclass """ """ Bibox exchange subclass """
import logging import logging
from typing import Dict from typing import Dict, List
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -24,3 +24,5 @@ class Bibox(Exchange):
def _ccxt_config(self) -> Dict: def _ccxt_config(self) -> Dict:
# Parameters to add directly to ccxt sync/async initialization. # Parameters to add directly to ccxt sync/async initialization.
return {"has": {"fetchCurrencies": False}} 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", "trades_pagination_arg": "fromId",
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], "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]] = [ _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list # TradingMode.SPOT always supported and not required in this list

View File

@ -1,7 +1,8 @@
""" Bybit exchange subclass """ """ Bybit exchange subclass """
import logging import logging
from typing import Dict from typing import Dict, List, Tuple
from freqtrade.enums import Collateral, TradingMode
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -21,3 +22,11 @@ class Bybit(Exchange):
_ft_has: Dict = { _ft_has: Dict = {
"ohlcv_candle_limit": 200, "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 copy import deepcopy
from datetime import datetime, timezone from datetime import datetime, timezone
from math import ceil from math import ceil
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple, Union
import arrow import arrow
import ccxt import ccxt
@ -72,6 +72,10 @@ class Exchange:
} }
_ft_has: Dict = {} _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]] = [ _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list # TradingMode.SPOT always supported and not required in this list
] ]
@ -207,7 +211,6 @@ class Exchange:
'secret': exchange_config.get('secret'), 'secret': exchange_config.get('secret'),
'password': exchange_config.get('password'), 'password': exchange_config.get('password'),
'uid': exchange_config.get('uid', ''), 'uid': exchange_config.get('uid', ''),
# 'options': exchange_config.get('options', {})
} }
if ccxt_kwargs: if ccxt_kwargs:
logger.info('Applying additional ccxt config: %s', 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, self._async_get_trade_history(pair=pair, since=since,
until=until, from_id=from_id)) 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): def fill_leverage_brackets(self):
""" """
# TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken # 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 Set's the leverage before making a trade, in order to not
have the same leverage on every trade 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"): if self._config['dry_run'] or not self.exchange_has("setLeverage"):
# Some exchanges only support one collateral type # Some exchanges only support one collateral type
return return

View File

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

View File

@ -1,6 +1,6 @@
""" Gate.io exchange subclass """ """ Gate.io exchange subclass """
import logging import logging
from typing import Dict from typing import Dict, List
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -23,3 +23,5 @@ class Gateio(Exchange):
} }
_headers = {'X-Gate-Channel-Id': 'freqtrade'} _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 import logging
from typing import Dict from typing import Dict, List
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -21,3 +21,5 @@ class Hitbtc(Exchange):
"ohlcv_candle_limit": 1000, "ohlcv_candle_limit": 1000,
"ohlcv_params": {"sort": "DESC"} "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": "id",
"trades_pagination_arg": "since", "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]] = [ _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list # TradingMode.SPOT always supported and not required in this list

View File

@ -1,6 +1,6 @@
""" Kucoin exchange subclass """ """ Kucoin exchange subclass """
import logging import logging
from typing import Dict from typing import Dict, List
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -24,3 +24,5 @@ class Kucoin(Exchange):
"order_time_in_force": ['gtc', 'fok', 'ioc'], "order_time_in_force": ['gtc', 'fok', 'ioc'],
"time_in_force_parameter": "timeInForce", "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 copy
import logging import logging
import traceback import traceback
from datetime import datetime, timezone from datetime import datetime, time, timezone
from math import isclose from math import isclose
from threading import Lock from threading import Lock
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import arrow import arrow
from schedule import Scheduler
from freqtrade import __version__, constants from freqtrade import __version__, constants
from freqtrade.configuration import validate_config_consistency from freqtrade.configuration import validate_config_consistency
from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge 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, from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError) InvalidOrderException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
@ -104,6 +105,25 @@ class FreqtradeBot(LoggingMixin):
self._exit_lock = Lock() self._exit_lock = Lock()
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) 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: def notify_status(self, msg: str) -> None:
""" """
Public method for users of this class (worker, etc.) to send notifications Public method for users of this class (worker, etc.) to send notifications
@ -183,7 +203,8 @@ class FreqtradeBot(LoggingMixin):
# Then looking for buy opportunities # Then looking for buy opportunities
if self.get_free_open_trades(): if self.get_free_open_trades():
self.enter_positions() self.enter_positions()
if self.trading_mode == TradingMode.FUTURES:
self._schedule.run_pending()
Trade.commit() Trade.commit()
def process_stopped(self) -> None: def process_stopped(self) -> None:
@ -239,6 +260,15 @@ class FreqtradeBot(LoggingMixin):
open_trades = len(Trade.get_open_trades()) open_trades = len(Trade.get_open_trades())
return max(0, self.config['max_open_trades'] - 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): def startup_update_open_orders(self):
""" """
Updates open orders based on order list kept in the database. 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}") 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): def update_closed_trades_without_assigned_fees(self):
""" """
Update closed trades without close fees assigned. Update closed trades without close fees assigned.
@ -571,6 +604,12 @@ class FreqtradeBot(LoggingMixin):
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') 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( trade = Trade(
pair=pair, pair=pair,
stake_amount=stake_amount, stake_amount=stake_amount,
@ -581,13 +620,15 @@ class FreqtradeBot(LoggingMixin):
fee_close=fee, fee_close=fee,
open_rate=enter_limit_filled_price, open_rate=enter_limit_filled_price,
open_rate_requested=enter_limit_requested, open_rate_requested=enter_limit_requested,
open_date=datetime.utcnow(), open_date=open_date,
exchange=self.exchange.id, exchange=self.exchange.id,
open_order_id=order_id, open_order_id=order_id,
strategy=self.strategy.get_strategy_name(), strategy=self.strategy.get_strategy_name(),
# TODO-lev: compatibility layer for buy_tag (!) # TODO-lev: compatibility layer for buy_tag (!)
buy_tag=enter_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) 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') strategy = get_column_def(cols, 'strategy', 'null')
buy_tag = get_column_def(cols, 'buy_tag', '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') 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') isolated_liq = get_column_def(cols, 'isolated_liq', 'null')
# sqlite does not support literals for booleans # sqlite does not support literals for booleans
is_short = get_column_def(cols, 'is_short', '0') 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 ticker-interval existed use that, else null.
if has_column(cols, 'ticker_interval'): if has_column(cols, 'ticker_interval'):
timeframe = get_column_def(cols, 'timeframe', '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, stoploss_order_id, stoploss_last_update,
max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag, 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 trading_mode, leverage, isolated_liq, is_short,
interest_rate, funding_fees
) )
select id, lower(exchange), pair, select id, lower(exchange), pair,
is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost, 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, {sell_order_status} sell_order_status,
{strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe, {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, {trading_mode} trading_mode, {leverage} leverage, {isolated_liq} isolated_liq,
{isolated_liq} isolated_liq, {is_short} is_short {is_short} is_short, {interest_rate} interest_rate,
{funding_fees} funding_fees
from {table_back_name} 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') table_back_name = get_backup_name(tabs, 'trades_bak')
# Check for latest column # 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}') logger.info(f'Running database migration for trades - backup: {table_back_name}')
migrate_trades_table(decl_base, inspector, engine, table_back_name, cols) migrate_trades_table(decl_base, inspector, engine, table_back_name, cols)
# Reread columns - the above recreated the table! # Reread columns - the above recreated the table!

View File

@ -6,7 +6,7 @@ from datetime import datetime, timedelta, timezone
from decimal import Decimal from decimal import Decimal
from typing import Any, Dict, List, Optional 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) create_engine, desc, func, inspect)
from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.exc import NoSuchModuleError
from sqlalchemy.orm import Query, declarative_base, relationship, scoped_session, sessionmaker 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 sqlalchemy.sql.schema import UniqueConstraint
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES 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.exceptions import DependencyException, OperationalException
from freqtrade.leverage import interest from freqtrade.leverage import interest
from freqtrade.misc import safe_value_fallback from freqtrade.misc import safe_value_fallback
@ -265,14 +265,19 @@ class LocalTrade():
buy_tag: Optional[str] = None buy_tag: Optional[str] = None
timeframe: Optional[int] = None timeframe: Optional[int] = None
trading_mode: TradingMode = TradingMode.SPOT
# Leverage trading properties # Leverage trading properties
is_short: bool = False
isolated_liq: Optional[float] = None isolated_liq: Optional[float] = None
is_short: bool = False
leverage: float = 1.0 leverage: float = 1.0
# Margin trading properties # Margin trading properties
interest_rate: float = 0.0 interest_rate: float = 0.0
# Futures properties
funding_fees: Optional[float] = None
@property @property
def has_no_leverage(self) -> bool: def has_no_leverage(self) -> bool:
"""Returns true if this is a non-leverage, non-short trade""" """Returns true if this is a non-leverage, non-short trade"""
@ -439,7 +444,8 @@ class LocalTrade():
'interest_rate': self.interest_rate, 'interest_rate': self.interest_rate,
'isolated_liq': self.isolated_liq, 'isolated_liq': self.isolated_liq,
'is_short': self.is_short, 'is_short': self.is_short,
'trading_mode': self.trading_mode,
'funding_fees': self.funding_fees,
'open_order_id': self.open_order_id, 'open_order_id': self.open_order_id,
} }
@ -643,7 +649,7 @@ class LocalTrade():
zero = Decimal(0.0) zero = Decimal(0.0)
# If nothing was borrowed # If nothing was borrowed
if self.has_no_leverage: if self.has_no_leverage or self.trading_mode != TradingMode.MARGIN:
return zero return zero
open_date = self.open_date.replace(tzinfo=None) 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) 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, def calc_close_trade_value(self, rate: Optional[float] = None,
fee: Optional[float] = None, fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float: interest_rate: Optional[float] = None) -> float:
@ -673,20 +690,32 @@ class LocalTrade():
if rate is None and not self.close_rate: if rate is None and not self.close_rate:
return 0.0 return 0.0
interest = self.calculate_interest(interest_rate) amount = Decimal(self.amount)
trading_mode = self.trading_mode or TradingMode.SPOT
if trading_mode == TradingMode.SPOT:
return float(self._calc_base_close(amount, rate, fee))
elif (trading_mode == TradingMode.MARGIN):
total_interest = self.calculate_interest(interest_rate)
if self.is_short: if self.is_short:
amount = Decimal(self.amount) + Decimal(interest) amount = amount + total_interest
return float(self._calc_base_close(amount, rate, fee))
else: else:
# Currency already owned for longs, no need to purchase # Currency already owned for longs, no need to purchase
amount = Decimal(self.amount) return float(self._calc_base_close(amount, rate, fee) - total_interest)
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
fees = close_trade * Decimal(fee or self.fee_close)
elif (trading_mode == TradingMode.FUTURES):
funding_fees = self.funding_fees or 0.0
if self.is_short: if self.is_short:
return float(close_trade + fees) return float(self._calc_base_close(amount, rate, fee)) - funding_fees
else: else:
return float(close_trade - fees - interest) return float(self._calc_base_close(amount, rate, fee)) + funding_fees
else:
raise OperationalException(
f"{self.trading_mode.value} trading is not yet available using freqtrade")
def calc_profit(self, rate: Optional[float] = None, def calc_profit(self, rate: Optional[float] = None,
fee: Optional[float] = None, fee: Optional[float] = None,
@ -894,6 +923,8 @@ class Trade(_DECL_BASE, LocalTrade):
buy_tag = Column(String(100), nullable=True) buy_tag = Column(String(100), nullable=True)
timeframe = Column(Integer, nullable=True) timeframe = Column(Integer, nullable=True)
trading_mode = Column(Enum(TradingMode), nullable=True)
# Leverage trading properties # Leverage trading properties
leverage = Column(Float, nullable=True, default=1.0) leverage = Column(Float, nullable=True, default=1.0)
is_short = Column(Boolean, nullable=False, default=False) is_short = Column(Boolean, nullable=False, default=False)
@ -902,6 +933,9 @@ class Trade(_DECL_BASE, LocalTrade):
# Margin Trading Properties # Margin Trading Properties
interest_rate = Column(Float, nullable=False, default=0.0) interest_rate = Column(Float, nullable=False, default=0.0)
# Futures properties
funding_fees = Column(Float, nullable=True, default=None)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.recalc_open_trade_value() self.recalc_open_trade_value()

View File

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

View File

@ -72,7 +72,8 @@ setup(
'fastapi', 'fastapi',
'uvicorn', 'uvicorn',
'pyjwt', 'pyjwt',
'aiofiles' 'aiofiles',
'schedule'
], ],
extras_require={ extras_require={
'dev': all_extra, 'dev': all_extra,

View File

@ -3048,6 +3048,74 @@ def test_calculate_backoff(retrycount, max_retries, expected):
assert 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('exchange', ['binance', 'kraken', 'ftx'])
@pytest.mark.parametrize('stake_amount,leverage,min_stake_with_lev', [ @pytest.mark.parametrize('stake_amount,leverage,min_stake_with_lev', [
(9.0, 3.0, 3.0), (9.0, 3.0, 3.0),

View File

@ -8,7 +8,7 @@ import pytest
from numpy import isnan from numpy import isnan
from freqtrade.edge import PairInfo 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.exceptions import ExchangeError, InvalidOrderException, TemporaryError
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.persistence.pairlock_middleware import PairLocks 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, 'interest_rate': 0.0,
'isolated_liq': None, 'isolated_liq': None,
'is_short': False, 'is_short': False,
'funding_fees': 0.0,
'trading_mode': TradingMode.SPOT
} }
mocker.patch('freqtrade.exchange.Exchange.get_rate', 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, 'interest_rate': 0.0,
'isolated_liq': None, 'isolated_liq': None,
'is_short': False, 'is_short': False,
'funding_fees': 0.0,
'trading_mode': TradingMode.SPOT
} }

View File

@ -11,7 +11,7 @@ import arrow
import pytest import pytest
from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT 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, from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, OperationalException, PricingError, InvalidOrderException, OperationalException, PricingError,
TemporaryError) 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 > custom_price_under_min_alwd
assert valid_price_at_min_alwd < proposed_price 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 sqlalchemy import create_engine, inspect, text
from freqtrade import constants from freqtrade import constants
from freqtrade.enums import TradingMode
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db 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, from tests.conftest import (create_mock_trades, create_mock_trades_with_leverage, get_sides,
log_has, log_has_re) log_has, log_has_re)
spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURES
def test_init_create_session(default_conf): def test_init_create_session(default_conf):
# Check if init create a session # Check if init create a session
init_db(default_conf['db_url'], default_conf['dry_run']) 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, fee_close=fee.return_value,
exchange='binance', exchange='binance',
is_short=is_short, is_short=is_short,
leverage=2.0 leverage=2.0,
trading_mode=margin
) )
assert trade.enter_side == enter_side assert trade.enter_side == enter_side
assert trade.exit_side == exit_side assert trade.exit_side == exit_side
@ -101,7 +106,8 @@ def test_set_stop_loss_isolated_liq(fee):
fee_close=fee.return_value, fee_close=fee.return_value,
exchange='binance', exchange='binance',
is_short=False, is_short=False,
leverage=2.0 leverage=2.0,
trading_mode=margin
) )
trade.set_isolated_liq(0.09) trade.set_isolated_liq(0.09)
assert trade.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 assert trade.initial_stop_loss == 0.09
@pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest', [ @pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest,trading_mode', [
("binance", False, 3, 10, 0.0005, round(0.0008333333333333334, 8)), ("binance", False, 3, 10, 0.0005, round(0.0008333333333333334, 8), margin),
("binance", True, 3, 10, 0.0005, 0.000625), ("binance", True, 3, 10, 0.0005, 0.000625, margin),
("binance", False, 3, 295, 0.0005, round(0.004166666666666667, 8)), ("binance", False, 3, 295, 0.0005, round(0.004166666666666667, 8), margin),
("binance", True, 3, 295, 0.0005, round(0.0031249999999999997, 8)), ("binance", True, 3, 295, 0.0005, round(0.0031249999999999997, 8), margin),
("binance", False, 3, 295, 0.00025, round(0.0020833333333333333, 8)), ("binance", False, 3, 295, 0.00025, round(0.0020833333333333333, 8), margin),
("binance", True, 3, 295, 0.00025, round(0.0015624999999999999, 8)), ("binance", True, 3, 295, 0.00025, round(0.0015624999999999999, 8), margin),
("binance", False, 5, 295, 0.0005, 0.005), ("binance", False, 5, 295, 0.0005, 0.005, margin),
("binance", True, 5, 295, 0.0005, round(0.0031249999999999997, 8)), ("binance", True, 5, 295, 0.0005, round(0.0031249999999999997, 8), margin),
("binance", False, 1, 295, 0.0005, 0.0), ("binance", False, 1, 295, 0.0005, 0.0, spot),
("binance", True, 1, 295, 0.0005, 0.003125), ("binance", True, 1, 295, 0.0005, 0.003125, margin),
("kraken", False, 3, 10, 0.0005, 0.040), ("binance", False, 3, 10, 0.0005, 0.0, futures),
("kraken", True, 3, 10, 0.0005, 0.030), ("binance", True, 3, 295, 0.0005, 0.0, futures),
("kraken", False, 3, 295, 0.0005, 0.06), ("binance", False, 5, 295, 0.0005, 0.0, futures),
("kraken", True, 3, 295, 0.0005, 0.045), ("binance", True, 5, 295, 0.0005, 0.0, futures),
("kraken", False, 3, 295, 0.00025, 0.03), ("binance", False, 1, 295, 0.0005, 0.0, futures),
("kraken", True, 3, 295, 0.00025, 0.0225), ("binance", True, 1, 295, 0.0005, 0.0, futures),
("kraken", False, 5, 295, 0.0005, round(0.07200000000000001, 8)),
("kraken", True, 5, 295, 0.0005, 0.045), ("kraken", False, 3, 10, 0.0005, 0.040, margin),
("kraken", False, 1, 295, 0.0005, 0.0), ("kraken", True, 3, 10, 0.0005, 0.030, margin),
("kraken", True, 1, 295, 0.0005, 0.045), ("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") @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 10min, 5hr limit trade on Binance/Kraken at 3x,5x leverage
fee: 0.25 % quote fee: 0.25 % quote
@ -258,21 +272,22 @@ def test_interest(market_buy_order_usdt, fee, exchange, is_short, lev, minutes,
exchange=exchange, exchange=exchange,
leverage=lev, leverage=lev,
interest_rate=rate, interest_rate=rate,
is_short=is_short is_short=is_short,
trading_mode=trading_mode
) )
assert round(float(trade.calculate_interest()), 8) == interest assert round(float(trade.calculate_interest()), 8) == interest
@pytest.mark.parametrize('is_short,lev,borrowed', [ @pytest.mark.parametrize('is_short,lev,borrowed,trading_mode', [
(False, 1.0, 0.0), (False, 1.0, 0.0, spot),
(True, 1.0, 30.0), (True, 1.0, 30.0, margin),
(False, 3.0, 40.0), (False, 3.0, 40.0, margin),
(True, 3.0, 30.0), (True, 3.0, 30.0, margin),
]) ])
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee, 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 10 minute limit trade on Binance/Kraken at 1x, 3x leverage
fee: 0.25% quote 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, fee_close=fee.return_value,
exchange='binance', exchange='binance',
is_short=is_short, is_short=is_short,
leverage=lev leverage=lev,
trading_mode=trading_mode
) )
assert trade.borrowed == borrowed assert trade.borrowed == borrowed
@pytest.mark.parametrize('is_short,open_rate,close_rate,lev,profit', [ @pytest.mark.parametrize('is_short,open_rate,close_rate,lev,profit,trading_mode', [
(False, 2.0, 2.2, 1.0, round(0.0945137157107232, 8)), (False, 2.0, 2.2, 1.0, round(0.0945137157107232, 8), spot),
(True, 2.2, 2.0, 3.0, round(0.2589996297562085, 8)) (True, 2.2, 2.0, 3.0, round(0.2589996297562085, 8), margin),
]) ])
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_usdt, 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 10 minute limit trade on Binance/Kraken at 1x, 3x leverage
fee: 0.25% quote fee: 0.25% quote
@ -445,7 +461,8 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_
exchange='binance', exchange='binance',
is_short=is_short, is_short=is_short,
interest_rate=0.0005, interest_rate=0.0005,
leverage=lev leverage=lev,
trading_mode=trading_mode
) )
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.close_profit 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, fee_close=fee.return_value,
open_date=arrow.utcnow().datetime, open_date=arrow.utcnow().datetime,
exchange='binance', exchange='binance',
trading_mode=margin
) )
trade.open_order_id = 'something' trade.open_order_id = 'something'
@ -518,20 +536,28 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
caplog) caplog)
@pytest.mark.parametrize('exchange,is_short,lev,open_value,close_value,profit,profit_ratio', [ @pytest.mark.parametrize(
("binance", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232), 'exchange,is_short,lev,open_value,close_value,profit,profit_ratio,trading_mode,funding_fees', [
("binance", True, 1, 59.850, 66.1663784375, -6.316378437500013, -0.1055368159983292), ("binance", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232, spot, 0.0),
("binance", False, 3, 60.15, 65.83416667, 5.684166670000003, 0.2834995845386534), ("binance", True, 1, 59.850, 66.1663784375, -6.3163784375, -0.105536815998329, margin, 0.0),
("binance", True, 3, 59.85, 66.1663784375, -6.316378437500013, -0.3166104479949876), ("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", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232, spot, 0.0),
("kraken", True, 1, 59.850, 66.231165, -6.381165, -0.106619298245614), ("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), ("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), ("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") @pytest.mark.usefixtures("init_persistence")
def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee, exchange, def test_calc_open_close_trade_price(
is_short, lev, open_value, close_value, profit, profit_ratio): 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( trade: Trade = Trade(
pair='ADA/USDT', pair='ADA/USDT',
stake_amount=60.0, 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, fee_close=fee.return_value,
exchange=exchange, exchange=exchange,
is_short=is_short, 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}' 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), open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10),
interest_rate=0.0005, interest_rate=0.0005,
exchange='binance', exchange='binance',
trading_mode=margin
) )
assert trade.close_profit is None assert trade.close_profit is None
assert trade.close_date 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_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
exchange='binance', exchange='binance',
trading_mode=margin
) )
trade.open_order_id = 'something' trade.open_order_id = 'something'
@ -617,6 +647,7 @@ def test_update_open_order(limit_buy_order_usdt):
fee_open=0.1, fee_open=0.1,
fee_close=0.1, fee_close=0.1,
exchange='binance', exchange='binance',
trading_mode=margin
) )
assert trade.open_order_id is None 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_open=0.1,
fee_close=0.1, fee_close=0.1,
exchange='binance', exchange='binance',
trading_mode=margin
) )
limit_buy_order_usdt['type'] = 'invalid' limit_buy_order_usdt['type'] = 'invalid'
with pytest.raises(ValueError, match=r'Unknown order type'): 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('exchange', ['binance', 'kraken'])
@pytest.mark.parametrize('trading_mode', [spot, margin, futures])
@pytest.mark.parametrize('lev', [1, 3]) @pytest.mark.parametrize('lev', [1, 3])
@pytest.mark.parametrize('is_short,fee_rate,result', [ @pytest.mark.parametrize('is_short,fee_rate,result', [
(False, 0.003, 60.18), (False, 0.003, 60.18),
@ -666,7 +699,8 @@ def test_calc_open_trade_value(
lev, lev,
is_short, is_short,
fee_rate, fee_rate,
result result,
trading_mode
): ):
# 10 minute limit trade on Binance/Kraken at 1x, 3x leverage # 10 minute limit trade on Binance/Kraken at 1x, 3x leverage
# fee: 0.25 %, 0.3% quote # fee: 0.25 %, 0.3% quote
@ -692,7 +726,8 @@ def test_calc_open_trade_value(
fee_close=fee_rate, fee_close=fee_rate,
exchange=exchange, exchange=exchange,
leverage=lev, leverage=lev,
is_short=is_short is_short=is_short,
trading_mode=trading_mode
) )
trade.open_order_id = 'open_trade' trade.open_order_id = 'open_trade'
@ -700,26 +735,37 @@ def test_calc_open_trade_value(
assert trade._calc_open_trade_value() == result assert trade._calc_open_trade_value() == result
@pytest.mark.parametrize('exchange,is_short,lev,open_rate,close_rate,fee_rate,result', [ @pytest.mark.parametrize(
('binance', False, 1, 2.0, 2.5, 0.0025, 74.8125), 'exchange,is_short,lev,open_rate,close_rate,fee_rate,result,trading_mode,funding_fees', [
('binance', False, 1, 2.0, 2.5, 0.003, 74.775), ('binance', False, 1, 2.0, 2.5, 0.0025, 74.8125, spot, 0),
('binance', False, 1, 2.0, 2.2, 0.005, 65.67), ('binance', False, 1, 2.0, 2.5, 0.003, 74.775, spot, 0),
('binance', False, 3, 2.0, 2.5, 0.0025, 74.81166667), ('binance', False, 1, 2.0, 2.2, 0.005, 65.67, margin, 0),
('binance', False, 3, 2.0, 2.5, 0.003, 74.77416667), ('binance', False, 3, 2.0, 2.5, 0.0025, 74.81166667, margin, 0),
('kraken', False, 3, 2.0, 2.5, 0.0025, 74.7725), ('binance', False, 3, 2.0, 2.5, 0.003, 74.77416667, margin, 0),
('kraken', False, 3, 2.0, 2.5, 0.003, 74.735), ('binance', True, 3, 2.2, 2.5, 0.0025, 75.18906641, margin, 0),
('kraken', True, 3, 2.2, 2.5, 0.0025, 75.2626875), ('binance', True, 3, 2.2, 2.5, 0.003, 75.22656719, margin, 0),
('kraken', True, 3, 2.2, 2.5, 0.003, 75.300225), ('binance', True, 1, 2.2, 2.5, 0.0025, 75.18906641, margin, 0),
('binance', True, 3, 2.2, 2.5, 0.0025, 75.18906641), ('binance', True, 1, 2.2, 2.5, 0.003, 75.22656719, margin, 0),
('binance', True, 3, 2.2, 2.5, 0.003, 75.22656719),
('binance', True, 1, 2.2, 2.5, 0.0025, 75.18906641), # Kraken
('binance', True, 1, 2.2, 2.5, 0.003, 75.22656719), ('kraken', False, 3, 2.0, 2.5, 0.0025, 74.7725, margin, 0),
('kraken', True, 1, 2.2, 2.5, 0.0025, 75.2626875), ('kraken', False, 3, 2.0, 2.5, 0.003, 74.735, margin, 0),
('kraken', True, 1, 2.2, 2.5, 0.003, 75.300225), ('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") @pytest.mark.usefixtures("init_persistence")
def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, open_rate, def test_calc_close_trade_price(
exchange, is_short, lev, close_rate, fee_rate, result): 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( trade = Trade(
pair='ADA/USDT', pair='ADA/USDT',
stake_amount=60.0, stake_amount=60.0,
@ -731,46 +777,82 @@ def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, ope
exchange=exchange, exchange=exchange,
interest_rate=0.0005, interest_rate=0.0005,
is_short=is_short, is_short=is_short,
leverage=lev leverage=lev,
trading_mode=trading_mode,
funding_fees=funding_fees
) )
trade.open_order_id = 'close_trade' trade.open_order_id = 'close_trade'
assert round(trade.calc_close_trade_value(rate=close_rate, fee=fee_rate), 8) == result 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', [ @pytest.mark.parametrize(
('binance', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673), 'exchange,is_short,lev,close_rate,fee_close,profit,profit_ratio,trading_mode,funding_fees', [
('binance', False, 3, 2.1, 0.0025, 2.69166667, 0.13424771421446402), ('binance', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673, spot, 0),
('binance', True, 1, 2.1, 0.0025, -3.308815781249997, -0.05528514254385963), ('binance', False, 3, 2.1, 0.0025, 2.69166667, 0.13424771421446402, margin, 0),
('binance', True, 3, 2.1, 0.0025, -3.308815781249997, -0.1658554276315789), ('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, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632, margin, 0),
('binance', False, 3, 1.9, 0.0025, -3.29333333, -0.16425602643391513), ('binance', False, 3, 1.9, 0.0025, -3.29333333, -0.16425602643391513, margin, 0),
('binance', True, 1, 1.9, 0.0025, 2.7063095312499996, 0.045218204365079395), ('binance', True, 1, 1.9, 0.0025, 2.7063095312499996, 0.045218204365079395, margin, 0),
('binance', True, 3, 1.9, 0.0025, 2.7063095312499996, 0.13565461309523819), ('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, 1, 2.2, 0.0025, 5.685, 0.0945137157107232, margin, 0),
('binance', False, 3, 2.2, 0.0025, 5.68416667, 0.2834995845386534), ('binance', False, 3, 2.2, 0.0025, 5.68416667, 0.2834995845386534, margin, 0),
('binance', True, 1, 2.2, 0.0025, -6.316378437499999, -0.1055368159983292), ('binance', True, 1, 2.2, 0.0025, -6.316378437499999, -0.1055368159983292, margin, 0),
('binance', True, 3, 2.2, 0.0025, -6.316378437499999, -0.3166104479949876), ('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
('kraken', False, 3, 2.1, 0.0025, 2.6525, 0.13229426433915248), ('kraken', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673, spot, 0),
('kraken', True, 1, 2.1, 0.0025, -3.3706575, -0.05631842105263152), ('kraken', False, 3, 2.1, 0.0025, 2.6525, 0.13229426433915248, margin, 0),
('kraken', True, 3, 2.1, 0.0025, -3.3706575, -0.16895526315789455), ('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, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632, margin, 0),
('kraken', False, 3, 1.9, 0.0025, -3.3325, -0.16620947630922667), ('kraken', False, 3, 1.9, 0.0025, -3.3325, -0.16620947630922667, margin, 0),
('kraken', True, 1, 1.9, 0.0025, 2.6503575, 0.04428333333333334), ('kraken', True, 1, 1.9, 0.0025, 2.6503575, 0.04428333333333334, margin, 0),
('kraken', True, 3, 1.9, 0.0025, 2.6503575, 0.13285000000000002), ('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, 1, 2.2, 0.0025, 5.685, 0.0945137157107232, margin, 0),
('kraken', False, 3, 2.2, 0.0025, 5.645, 0.2815461346633419), ('kraken', False, 3, 2.2, 0.0025, 5.645, 0.2815461346633419, margin, 0),
('kraken', True, 1, 2.2, 0.0025, -6.381165, -0.106619298245614), ('kraken', True, 1, 2.2, 0.0025, -6.381165, -0.106619298245614, margin, 0),
('kraken', True, 3, 2.2, 0.0025, -6.381165, -0.319857894736842), ('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, 2.1, 0.003, 2.6610000000000014, 0.04423940149625927, spot, 0),
('binance', False, 1, 1.9, 0.003, -3.320999999999998, -0.05521197007481293), ('binance', False, 1, 1.9, 0.003, -3.320999999999998, -0.05521197007481293, spot, 0),
('binance', False, 1, 2.2, 0.003, 5.652000000000008, 0.09396508728179565), ('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") @pytest.mark.usefixtures("init_persistence")
def test_calc_profit( def test_calc_profit(
@ -783,7 +865,9 @@ def test_calc_profit(
close_rate, close_rate,
fee_close, fee_close,
profit, profit,
profit_ratio profit_ratio,
trading_mode,
funding_fees
): ):
""" """
10 minute limit trade on Binance/Kraken at 1x, 3x leverage 10 minute limit trade on Binance/Kraken at 1x, 3x leverage
@ -802,6 +886,7 @@ def test_calc_profit(
1x,-1x: 60.0 quote 1x,-1x: 60.0 quote
3x,-3x: 20.0 quote 3x,-3x: 20.0 quote
hours: 1/6 (10 minutes) hours: 1/6 (10 minutes)
funding_fees: 1
borrowed borrowed
1x: 0 quote 1x: 0 quote
3x: 40 quote 3x: 40 quote
@ -913,6 +998,87 @@ def test_calc_profit(
2.1 quote: (62.811 / 60.15) - 1 = 0.04423940149625927 2.1 quote: (62.811 / 60.15) - 1 = 0.04423940149625927
1.9 quote: (56.829 / 60.15) - 1 = -0.05521197007481293 1.9 quote: (56.829 / 60.15) - 1 = -0.05521197007481293
2.2 quote: (65.802 / 60.15) - 1 = 0.09396508728179565 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( trade = Trade(
pair='ADA/USDT', pair='ADA/USDT',
@ -925,7 +1091,9 @@ def test_calc_profit(
is_short=is_short, is_short=is_short,
leverage=lev, leverage=lev,
fee_open=0.0025, 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' trade.open_order_id = 'something'
@ -1439,6 +1607,8 @@ def test_to_json(default_conf, fee):
'interest_rate': None, 'interest_rate': None,
'isolated_liq': None, 'isolated_liq': None,
'is_short': None, 'is_short': None,
'trading_mode': None,
'funding_fees': None
} }
# Simulate dry_run entries # Simulate dry_run entries
@ -1510,6 +1680,8 @@ def test_to_json(default_conf, fee):
'interest_rate': None, 'interest_rate': None,
'isolated_liq': None, 'isolated_liq': None,
'is_short': None, 'is_short': None,
'trading_mode': None,
'funding_fees': None
} }