Merge pull request #5377 from samgermain/funding-fee
Funding Fee (Futures)
This commit is contained in:
commit
aed138ba03
@ -29,7 +29,7 @@ dependencies:
|
||||
- colorama
|
||||
- questionary
|
||||
- prompt-toolkit
|
||||
|
||||
- schedule
|
||||
|
||||
# ============================
|
||||
# 2/4 req dev
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
@ -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)
|
||||
|
||||
|
@ -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!
|
||||
|
@ -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)
|
||||
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:
|
||||
amount = Decimal(self.amount) + Decimal(interest)
|
||||
amount = amount + total_interest
|
||||
return float(self._calc_base_close(amount, rate, fee))
|
||||
else:
|
||||
# Currency already owned for longs, no need to purchase
|
||||
amount = Decimal(self.amount)
|
||||
|
||||
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
|
||||
fees = close_trade * Decimal(fee or self.fee_close)
|
||||
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(close_trade + fees)
|
||||
return float(self._calc_base_close(amount, rate, fee)) - funding_fees
|
||||
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,
|
||||
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()
|
||||
|
@ -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
|
||||
|
7
setup.py
7
setup.py
@ -11,7 +11,7 @@ hyperopt = [
|
||||
'joblib',
|
||||
'progressbar2',
|
||||
'psutil',
|
||||
]
|
||||
]
|
||||
|
||||
develop = [
|
||||
'coveralls',
|
||||
@ -31,7 +31,7 @@ jupyter = [
|
||||
'nbstripout',
|
||||
'ipykernel',
|
||||
'nbconvert',
|
||||
]
|
||||
]
|
||||
|
||||
all_extra = plot + develop + jupyter + hyperopt
|
||||
|
||||
@ -72,7 +72,8 @@ setup(
|
||||
'fastapi',
|
||||
'uvicorn',
|
||||
'pyjwt',
|
||||
'aiofiles'
|
||||
'aiofiles',
|
||||
'schedule'
|
||||
],
|
||||
extras_require={
|
||||
'dev': all_extra,
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user