Created FundingFee class and added funding_fee to LocalTrade and freqtradebot

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

View File

@ -1,8 +1,11 @@
# flake8: noqa: F401 # flake8: noqa: F401
from freqtrade.enums.backteststate import BacktestState from freqtrade.enums.backteststate import BacktestState
from freqtrade.enums.collateral import Collateral
from freqtrade.enums.interestmode import InterestMode from freqtrade.enums.interestmode import InterestMode
from freqtrade.enums.liqformula import LiqFormula
from freqtrade.enums.rpcmessagetype import RPCMessageType from freqtrade.enums.rpcmessagetype import RPCMessageType
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
from freqtrade.enums.selltype import SellType from freqtrade.enums.selltype import SellType
from freqtrade.enums.signaltype import SignalTagType, SignalType from freqtrade.enums.signaltype import SignalTagType, SignalType
from freqtrade.enums.state import State from freqtrade.enums.state import State
from freqtrade.enums.tradingmode import TradingMode

View File

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

View File

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

View File

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

View File

@ -16,10 +16,11 @@ from freqtrade.configuration import validate_config_consistency
from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.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
from freqtrade.leverage import FundingFee
from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.misc import safe_value_fallback, safe_value_fallback2
from freqtrade.mixins import LoggingMixin from freqtrade.mixins import LoggingMixin
from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db
@ -102,6 +103,11 @@ class FreqtradeBot(LoggingMixin):
self._sell_lock = Lock() self._sell_lock = Lock()
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) 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: 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
@ -553,6 +559,10 @@ class FreqtradeBot(LoggingMixin):
amount = safe_value_fallback(order, 'filled', 'amount') amount = safe_value_fallback(order, 'filled', 'amount')
buy_limit_filled_price = safe_value_fallback(order, 'average', 'price') 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 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')
trade = Trade( trade = Trade(
@ -570,10 +580,15 @@ class FreqtradeBot(LoggingMixin):
open_order_id=order_id, open_order_id=order_id,
strategy=self.strategy.get_strategy_name(), strategy=self.strategy.get_strategy_name(),
buy_tag=buy_tag, 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) trade.orders.append(order_obj)
if self.trading_mode == TradingMode.FUTURES:
self.funding_fee.add_new_trade(trade)
# Update fees if order is closed # Update fees if order is closed
if order_status == 'closed': if order_status == 'closed':
self.update_trade_state(trade, order_id, order) self.update_trade_state(trade, order_id, order)

View File

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

View File

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

View File

@ -49,12 +49,18 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
strategy = get_column_def(cols, 'strategy', 'null') 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 = 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
is_short = get_column_def(cols, 'is_short', '0') 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_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 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')
@ -66,7 +72,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
close_profit_abs = get_column_def( close_profit_abs = get_column_def(
cols, 'close_profit_abs', cols, 'close_profit_abs',
f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}") 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') sell_order_status = get_column_def(cols, 'sell_order_status', 'null')
amount_requested = get_column_def(cols, 'amount_requested', 'amount') amount_requested = get_column_def(cols, 'amount_requested', 'amount')
@ -92,7 +98,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
stoploss_order_id, stoploss_last_update, 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, 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, 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,
@ -109,9 +116,10 @@ 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, {liq_formula} liq_formula, {interest_mode} interest_mode,
{interest_mode} interest_mode {interest_rate} interest_rate, {funding_fee} funding_fee,
{last_funding_adjustment} last_funding_adjustment
from {table_back_name} from {table_back_name}
""")) """))
@ -171,7 +179,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
table_back_name = get_backup_name(tabs, 'trades_bak') 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, 'buy_tag'):
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

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

View File

@ -41,3 +41,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.19 prompt-toolkit==3.0.19
#Futures
schedule==1.1.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
@ -108,10 +108,15 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stoploss_entry_dist_ratio': -0.10448878, 'stoploss_entry_dist_ratio': -0.10448878,
'open_order': None, 'open_order': None,
'exchange': 'binance', 'exchange': 'binance',
'leverage': 1.0, 'trading_mode': TradingMode.SPOT,
'interest_rate': 0.0,
'isolated_liq': None, 'isolated_liq': None,
'is_short': False, 'is_short': False,
'leverage': 1.0,
'liq_formula': None,
'interest_mode': None,
'interest_rate': 0.0,
'funding_fee': None,
'last_funding_adjustment': None,
} }
mocker.patch('freqtrade.exchange.Exchange.get_rate', mocker.patch('freqtrade.exchange.Exchange.get_rate',
@ -179,10 +184,15 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stoploss_entry_dist_ratio': -0.10448878, 'stoploss_entry_dist_ratio': -0.10448878,
'open_order': None, 'open_order': None,
'exchange': 'binance', 'exchange': 'binance',
'leverage': 1.0, 'trading_mode': TradingMode.SPOT,
'interest_rate': 0.0,
'isolated_liq': None, 'isolated_liq': None,
'is_short': False, 'is_short': False,
'leverage': 1.0,
'liq_formula': None,
'interest_mode': None,
'interest_rate': 0.0,
'funding_fee': None,
'last_funding_adjustment': None,
} }

View File

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