8000 Funding Fee by samgermain · Pull Request #5364 · freqtrade/freqtrade · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Funding Fee #5364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions freqtrade/enums/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# flake8: noqa: F401
from freqtrade.enums.backteststate import BacktestState
from freqtrade.enums.collateral import Collateral
from freqtrade.enums.interestmode import InterestMode
from freqtrade.enums.liqformula import LiqFormula
from freqtrade.enums.rpcmessagetype import RPCMessageType
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
from freqtrade.enums.selltype import SellType
from freqtrade.enums.signaltype import SignalTagType, SignalType
from freqtrade.enums.state import State
from freqtrade.enums.tradingmode import TradingMode
11 changes: 11 additions & 0 deletions freqtrade/enums/collateral.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from enum import Enum


class Collateral(Enum):
"""
Enum to distinguish between
cross margin/futures collateral and
isolated margin/futures collateral
"""
CROSS = "cross"
ISOLATED = "isolated"
108 changes: 108 additions & 0 deletions freqtrade/enums/liqformula.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# from decimal import Decimal
from enum import Enum

from freqtrade.enums.collateral import Collateral
from freqtrade.enums.tradingmode import TradingMode
from freqtrade.exceptions import OperationalException


# from math import ceil


class LiqFormula(Enum):
"""Equations to calculate liquidation price"""

BINANCE = "Binance"
KRAKEN = "Kraken"
FTX = "FTX"
NONE = None

def __call__(self, **k):

trading_mode: TradingMode = k['trading_mode']
if trading_mode == TradingMode.SPOT or self.name == "NONE":
return None

collateral: Collateral = k['collateral']

if self.name == "BINANCE":
return binance(trading_mode, collateral)
elif self.name == "KRAKEN":
return kraken(trading_mode, collateral)
elif self.name == "FTX":
return ftx(trading_mode, collateral)
else:
exception(self.name, trading_mode, collateral)


def exception(name: str, trading_mode: TradingMode, collateral: Collateral):
"""
Raises an exception if exchange used doesn't support desired leverage mode
:param name: Name of the exchange
:param trading_mode: spot, margin, futures
:param collateral: cross, isolated
"""
raise OperationalException(
f"{name} does not support {collateral.value} {trading_mode.value} trading")


def binance(name: str, trading_mode: TradingMode, collateral: Collateral):
"""
Calculates the liquidation price on Binance
:param name: Name of the exchange
:param trading_mode: spot, margin, futures
:param collateral: cross, isolated
"""
# TODO-lev: Additional arguments, fill in formulas

if trading_mode == TradingMode.MARGIN and collateral == Collateral.CROSS:
# TODO-lev: perform a calculation based on this formula
# https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed
exception(name, trading_mode, collateral)
elif trading_mode == TradingMode.FUTURES and collateral == Collateral.CROSS:
# TODO-lev: perform a calculation based on this formula
# https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
exception(name, trading_mode, collateral)
elif trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED:
# TODO-lev: perform a calculation based on this formula
# https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
exception(name, trading_mode, collateral)

# If nothing was returned
exception(name, trading_mode, collateral)


def kraken(name: str, trading_mode: TradingMode, collateral: Collateral):
"""
Calculates the liquidation price on Kraken
:param name: Name of the exchange
:param trading_mode: spot, margin, futures
:param collateral: cross, isolated
"""
# TODO-lev: Additional arguments, fill in formulas

if collateral == Collateral.CROSS:
if trading_mode == TradingMode.MARGIN:
exception(name, trading_mode, collateral)
# TODO-lev: perform a calculation based on this formula
# https://support.kraken.com/hc/en-us/articles/203325763-Margin-Call-Level-and-Margin-Liquidation-Level
elif trading_mode == TradingMode.FUTURES:
exception(name, trading_mode, collateral)

# If nothing was returned
exception(name, trading_mode, collateral)


def ftx(name: str, trading_mode: TradingMode, collateral: Collateral):
"""
Calculates the liquidation price on FTX
:param name: Name of the exchange
:param trading_mode: spot, margin, futures
:param collateral: cross, isolated
"""
if collateral == Collateral.CROSS:
# TODO-lev: Additional arguments, fill in formulas
exception(name, trading_mode, collateral)

# If nothing was returned
exception(name, trading_mode, collateral)
11 changes: 11 additions & 0 deletions freqtrade/enums/tradingmode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from enum import Enum


class TradingMode(Enum):
"""
Enum to distinguish between
spot, margin, futures or any other trading method
"""
SPOT = "spot"
MARGIN = "margin"
FUTURES = "futures"
19 changes: 17 additions & 2 deletions freqtrade/freqtradebot.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge
from freqtrade.enums import RPCMessageType, SellType, State
from freqtrade.enums import RPCMessageType, SellType, State, TradingMode
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.leverage import FundingFee
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
from freqtrade.mixins import LoggingMixin
from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db
Expand Down Expand Up @@ -102,6 +103,11 @@ def __init__(self, config: Dict[str, Any]) -> None:
self._sell_lock = Lock()
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))

self.trading_mode = TradingMode.SPOT
if self.trading_mode == TradingMode.FUTURES:
self.funding_fee = FundingFee()
self.funding_fee.start()

def notify_status(self, msg: str) -> None:
"""
Public method for users of this class (worker, etc.) to send notifications
Expand Down Expand Up @@ -553,6 +559,10 @@ def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = N
amount = safe_value_fallback(order, 'filled', 'amount')
buy_limit_filled_price = safe_value_fallback(order, 'average', 'price')

funding_fee = (self.funding_fee.initial_funding_fee(amount)
if self.trading_mode == TradingMode.FUTURES
else None)

# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
trade = Trade(
Expand All @@ -570,10 +580,15 @@ def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = N
open_order_id=order_id,
strategy=self.strategy.get_strategy_name(),
buy_tag=buy_tag,
timeframe=timeframe_to_minutes(self.config['timeframe'])
timeframe=timeframe_to_minutes(self.config['timeframe']),
funding_fee=funding_fee,
trading_mode=self.trading_mode
)
trade.orders.append(order_obj)

if self.trading_mode == TradingMode.FUTURES:
self.funding_fee.add_new_trade(trade)

# Update fees if order is closed
if order_status == 'closed':
self.update_trade_state(trade, order_id, order)
Expand Down
2 changes: 2 additions & 0 deletions freqtrade/leverage/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# flake8: noqa: F401
from freqtrade.leverage.funding_fee import FundingFee
71 changes: 71 additions & 0 deletions 71 freqtrade/leverage/funding_fee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from datetime import datetime, timedelta
from typing import List

import schedule

from freqtrade.persistence import Trade


class FundingFee:

trades: List[Trade]
begin_times = [
# TODO-lev: Make these UTC time
"23:59:45",
"07:59:45",
"15:59:45",
]

def _is_time_between(self, begin_time, end_time):
# If check time is not given, default to current UTC time
check_time = datetime.utcnow().time()
if begin_time < end_time:
return check_time >= begin_time and check_time <= end_time
else: # crosses midnight
return check_time >= begin_time or check_time <= end_time

def _apply_funding_fees(self, num_of: int = 1):
if num_of == 0:
return
for trade in self.trades:
trade.adjust_funding_fee(self._calculate(trade.amount) * num_of)

def _calculate(self, amount):
# TODO-futures: implement
# TODO-futures: Check how other exchages do it and adjust accordingly
# https://www.binance.com/en/support/faq/360033525031
# mark_price =
# contract_size = maybe trade.amount
# funding_rate = # https://www.binance.com/en/futures/funding-history/0
# nominal_value = mark_price * contract_size
# adjustment = nominal_value * funding_rate
# return adjustment
return

def initial_funding_fee(self, amount) -> float:
# A funding fee interval is applied immediately if within 30s of an iterval
for begin_string in self.begin_times:
begin_time = datetime.strptime(begin_string, "%H:%M:%S")
end_time = (begin_time + timedelta(seconds=30))
if self._is_time_between(begin_time.time(), end_time.time()):
return self._calculate(amount)
return 0.0

def start(self):
for interval in self.begin_times:
schedule.every().day.at(interval).do(self._apply_funding_fees())

# https://stackoverflow.com/a/30393162/6331353
# TODO-futures: Put schedule.run_pending() somewhere in the bot_loop

def reboot(self):
# TODO-futures Find out how many begin_times have passed since last funding_fee added
amount_missed = 0
self.apply_funding_fees(num_of=amount_missed)
self.start()

def add_new_trade(self, trade):
self.trades.append(trade)

def remove_trade(self, trade):
self.trades.remove(trade)
24 changes: 16 additions & 8 deletions freqtrade/persistence/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,18 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
strategy = get_column_def(cols, 'strategy', 'null')
buy_tag = get_column_def(cols, 'buy_tag', 'null')

trading_mode = get_column_def(cols, 'trading_mode', 'null')
leverage = get_column_def(cols, 'leverage', '1.0')
interest_rate = get_column_def(cols, 'interest_rate', '0.0')
isolated_liq = get_column_def(cols, 'isolated_liq', 'null')
# sqlite does not support literals for booleans
is_short = get_column_def(cols, 'is_short', '0')
liq_formula = get_column_def(cols, 'liq_formula', 'null')
# sqlite does not support literals for booleans
interest_mode = get_column_def(cols, 'interest_mode', 'null')
interest_rate = get_column_def(cols, 'interest_rate', '0.0')

funding_fee = get_column_def(cols, 'funding_fee', '0.0')
last_funding_adjustment = get_column_def(cols, 'last_funding_adjustment', 'null')

# If ticker-interval existed use that, else null.
if has_column(cols, 'ticker_interval'):
timeframe = get_column_def(cols, 'timeframe', 'ticker_interval')
Expand All @@ -66,7 +72,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
close_profit_abs = get_column_def(
cols, 'close_profit_abs',
f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}")
# TODO-mg: update to exit order status
# TODO-lev: update to exit order status
sell_order_status = get_column_def(cols, 'sell_order_status', 'null')
amount_requested = get_column_def(cols, 'amount_requested', 'amount')

Expand All @@ -92,7 +98,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
stoploss_order_id, stoploss_last_update,
max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag,
timeframe, open_trade_value, close_profit_abs,
leverage, interest_rate, isolated_liq, is_short, interest_mode
trading_mode, leverage, isolated_liq, is_short, liq_formula, interest_mode,
interest_rate, funding_fee, last_funding_adjustment
)
select id, lower(exchange), pair,
is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
Expand All @@ -109,9 +116,10 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
{sell_order_status} sell_order_status,
{strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe,
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
{leverage} leverage, {interest_rate} interest_rate,
{isolated_liq} isolated_liq, {is_short} is_short,
{interest_mode} interest_mode
{trading_mode} trading_mode, {leverage} leverage, {isolated_liq} isolated_liq,
{is_short} is_short, {liq_formula} liq_formula, {interest_mode} interest_mode,
{interest_rate} interest_rate, {funding_fee} funding_fee,
{last_funding_adjustment} last_funding_adjustment
from {table_back_name}
"""))

Expand Down Expand Up @@ -171,7 +179,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
table_back_name = get_backup_name(tabs, 'trades_bak')

# Check for latest column
if not has_column(cols, 'is_short'):
if not has_column(cols, 'buy_tag'):
logger.info(f'Running database migration for trades - backup: {table_back_name}')
migrate_trades_table(decl_base, inspector, engine, table_back_name, cols)
# Reread columns - the above recreated the table!
Expand Down
Loading
0