Created FundingFee class and added funding_fee to LocalTrade and freqtradebot
This commit is contained in:
parent
797d7e5ce6
commit
e3845ff808
@ -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
freqtrade/enums/collateral.py
Normal file
11
freqtrade/enums/collateral.py
Normal 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"
|
108
freqtrade/enums/liqformula.py
Normal file
108
freqtrade/enums/liqformula.py
Normal 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)
|
11
freqtrade/enums/tradingmode.py
Normal file
11
freqtrade/enums/tradingmode.py
Normal 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"
|
@ -16,10 +16,11 @@ 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
|
||||
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
|
||||
@ -102,6 +103,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
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
|
||||
@ -553,6 +559,10 @@ class FreqtradeBot(LoggingMixin):
|
||||
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(
|
||||
@ -570,10 +580,15 @@ class FreqtradeBot(LoggingMixin):
|
||||
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)
|
||||
|
2
freqtrade/leverage/__init__.py
Normal file
2
freqtrade/leverage/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# flake8: noqa: F401
|
||||
from freqtrade.leverage.funding_fee import FundingFee
|
71
freqtrade/leverage/funding_fee.py
Normal file
71
freqtrade/leverage/funding_fee.py
Normal 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)
|
@ -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')
|
||||
@ -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')
|
||||
|
||||
@ -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,
|
||||
@ -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}
|
||||
"""))
|
||||
|
||||
@ -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!
|
||||
|
@ -2,7 +2,7 @@
|
||||
This module contains the class to persist trades into SQLite
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
@ -14,7 +14,7 @@ from sqlalchemy.pool import StaticPool
|
||||
from sqlalchemy.sql.schema import UniqueConstraint
|
||||
|
||||
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.misc import safe_value_fallback
|
||||
from freqtrade.persistence.migrations import check_migrate
|
||||
@ -92,6 +92,12 @@ def clean_dry_run_db() -> None:
|
||||
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):
|
||||
"""
|
||||
Order database model
|
||||
@ -264,12 +270,21 @@ class LocalTrade():
|
||||
buy_tag: Optional[str] = None
|
||||
timeframe: Optional[int] = None
|
||||
|
||||
# Margin trading properties
|
||||
interest_rate: float = 0.0
|
||||
trading_mode: TradingMode = TradingMode.SPOT
|
||||
|
||||
# Leverage trading properties
|
||||
isolated_liq: Optional[float] = None
|
||||
is_short: bool = False
|
||||
leverage: float = 1.0
|
||||
liq_formula: LiqFormula = LiqFormula.NONE
|
||||
|
||||
# Margin trading properties
|
||||
interest_mode: InterestMode = InterestMode.NONE
|
||||
interest_rate: float = 0.0
|
||||
|
||||
# Futures properties
|
||||
funding_fee: Optional[float] = None
|
||||
last_funding_adjustment: Optional[datetime] = None
|
||||
|
||||
@property
|
||||
def has_no_leverage(self) -> bool:
|
||||
@ -316,7 +331,7 @@ class LocalTrade():
|
||||
for key in kwargs:
|
||||
setattr(self, key, kwargs[key])
|
||||
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()
|
||||
|
||||
def _set_stop_loss(self, stop_loss: float, percent: float):
|
||||
@ -342,11 +357,17 @@ class LocalTrade():
|
||||
self.stop_loss_pct = -1 * abs(percent)
|
||||
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.
|
||||
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.is_short:
|
||||
self.stop_loss = min(self.stop_loss, isolated_liq)
|
||||
@ -432,11 +453,16 @@ class LocalTrade():
|
||||
'min_rate': self.min_rate,
|
||||
'max_rate': self.max_rate,
|
||||
|
||||
'leverage': self.leverage,
|
||||
'interest_rate': self.interest_rate,
|
||||
'isolated_liq': self.isolated_liq,
|
||||
'is_short': self.is_short,
|
||||
|
||||
"trading_mode": self.trading_mode,
|
||||
"isolated_liq": self.isolated_liq,
|
||||
"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,
|
||||
}
|
||||
|
||||
@ -513,6 +539,10 @@ class LocalTrade():
|
||||
f"Trailing stoploss saved us: "
|
||||
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:
|
||||
"""
|
||||
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']:
|
||||
if self.is_open:
|
||||
payment = "BUY" if self.is_short else "SELL"
|
||||
# TODO-mg: On shorts, you buy a little bit more than the amount (amount + interest)
|
||||
# This wll only print the original amount
|
||||
# TODO-lev: On shorts, you buy a little bit more than the amount (amount + interest)
|
||||
# TODO-lev: This wll only print the original amount
|
||||
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'):
|
||||
self.stoploss_order_id = None
|
||||
self.close_rate_requested = self.stop_loss
|
||||
@ -650,8 +681,20 @@ class LocalTrade():
|
||||
rate = Decimal(interest_rate or self.interest_rate)
|
||||
borrowed = Decimal(self.borrowed)
|
||||
|
||||
# TODO-lev: Pass trading mode to interest_mode maybe
|
||||
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,
|
||||
fee: Optional[float] = None,
|
||||
interest_rate: Optional[float] = None) -> float:
|
||||
@ -665,23 +708,36 @@ class LocalTrade():
|
||||
If interest_rate is not set self.interest_rate will be used
|
||||
:return: Price in BTC of the open trade
|
||||
"""
|
||||
|
||||
if rate is None and not self.close_rate:
|
||||
return 0.0
|
||||
|
||||
interest = self.calculate_interest(interest_rate)
|
||||
if self.is_short:
|
||||
amount = Decimal(self.amount) + Decimal(interest)
|
||||
else:
|
||||
# Currency already owned for longs, no need to purchase
|
||||
amount = Decimal(self.amount)
|
||||
amount = Decimal(self.amount)
|
||||
trading_mode = self.trading_mode or TradingMode.SPOT
|
||||
|
||||
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
|
||||
fees = close_trade * Decimal(fee or self.fee_close)
|
||||
if trading_mode == TradingMode.SPOT:
|
||||
return float(self._calc_base_close(amount, rate, fee))
|
||||
|
||||
if self.is_short:
|
||||
return float(close_trade + fees)
|
||||
elif (trading_mode == TradingMode.MARGIN):
|
||||
|
||||
interest = self.calculate_interest(interest_rate)
|
||||
|
||||
if self.is_short:
|
||||
amount = amount + interest
|
||||
return float(self._calc_base_close(amount, rate, fee))
|
||||
else:
|
||||
# Currency already owned for longs, no need to purchase
|
||||
return float(self._calc_base_close(amount, rate, fee) - interest)
|
||||
|
||||
elif (trading_mode == TradingMode.FUTURES):
|
||||
funding_fee = self.funding_fee or 0.0
|
||||
if self.is_short:
|
||||
return float(self._calc_base_close(amount, rate, fee)) + funding_fee
|
||||
else:
|
||||
return float(self._calc_base_close(amount, rate, fee)) - funding_fee
|
||||
else:
|
||||
return float(close_trade - fees - interest)
|
||||
raise OperationalException(
|
||||
f"{self.trading_mode.value} trading is not yet available using freqtrade")
|
||||
|
||||
def calc_profit(self, rate: Optional[float] = None,
|
||||
fee: Optional[float] = None,
|
||||
@ -883,19 +939,27 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
max_rate = Column(Float, nullable=True, default=0.0)
|
||||
# Lowest price reached
|
||||
min_rate = Column(Float, nullable=True)
|
||||
sell_reason = Column(String(100), nullable=True) # TODO-mg: Change to close_reason
|
||||
sell_order_status = Column(String(100), nullable=True) # TODO-mg: Change to close_order_status
|
||||
sell_reason = Column(String(100), nullable=True) # TODO-lev: Change to close_reason
|
||||
sell_order_status = Column(String(100), nullable=True) # TODO-lev: Change to close_order_status
|
||||
strategy = Column(String(100), nullable=True)
|
||||
buy_tag = Column(String(100), nullable=True)
|
||||
timeframe = Column(Integer, nullable=True)
|
||||
|
||||
# Margin trading properties
|
||||
leverage = Column(Float, nullable=True, default=1.0)
|
||||
interest_rate = Column(Float, nullable=False, default=0.0)
|
||||
trading_mode = Column(Enum(TradingMode))
|
||||
|
||||
# Leverage trading properties
|
||||
isolated_liq = Column(Float, nullable=True)
|
||||
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)
|
||||
# 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):
|
||||
super().__init__(**kwargs)
|
||||
|
@ -41,3 +41,6 @@ colorama==0.4.4
|
||||
# Building config files interactively
|
||||
questionary==1.10.0
|
||||
prompt-toolkit==3.0.19
|
||||
|
||||
#Futures
|
||||
schedule==1.1.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
|
||||
@ -108,10 +108,15 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'stoploss_entry_dist_ratio': -0.10448878,
|
||||
'open_order': None,
|
||||
'exchange': 'binance',
|
||||
'leverage': 1.0,
|
||||
'interest_rate': 0.0,
|
||||
'trading_mode': TradingMode.SPOT,
|
||||
'isolated_liq': None,
|
||||
'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',
|
||||
@ -179,10 +184,15 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'stoploss_entry_dist_ratio': -0.10448878,
|
||||
'open_order': None,
|
||||
'exchange': 'binance',
|
||||
'leverage': 1.0,
|
||||
'interest_rate': 0.0,
|
||||
'trading_mode': TradingMode.SPOT,
|
||||
'isolated_liq': None,
|
||||
'is_short': False,
|
||||
'leverage': 1.0,
|
||||
'liq_formula': None,
|
||||
'interest_mode': None,
|
||||
'interest_rate': 0.0,
|
||||
'funding_fee': None,
|
||||
'last_funding_adjustment': None,
|
||||
}
|
||||
|
||||
|
||||
|
@ -11,7 +11,7 @@ import pytest
|
||||
from sqlalchemy import create_engine, inspect, text
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.enums import InterestMode
|
||||
from freqtrade.enums import InterestMode, 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, log_has, log_has_re
|
||||
@ -91,7 +91,7 @@ def test_enter_exit_side(fee):
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test__set_stop_loss_isolated_liq(fee):
|
||||
def test_set_stop_loss_isolated_liq(fee):
|
||||
trade = Trade(
|
||||
id=2,
|
||||
pair='ADA/USDT',
|
||||
@ -106,7 +106,7 @@ def test__set_stop_loss_isolated_liq(fee):
|
||||
is_short=False,
|
||||
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.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.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.stop_loss == 0.1
|
||||
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.stop_loss == 0.11
|
||||
assert trade.initial_stop_loss == 0.09
|
||||
@ -145,7 +145,7 @@ def test__set_stop_loss_isolated_liq(fee):
|
||||
trade.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.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.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.stop_loss == 0.08
|
||||
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.stop_loss == 0.07
|
||||
assert trade.initial_stop_loss == 0.09
|
||||
@ -237,7 +237,8 @@ def test_interest(market_buy_order_usdt, fee):
|
||||
exchange='kraken',
|
||||
leverage=3.0,
|
||||
interest_rate=0.0005,
|
||||
interest_mode=InterestMode.HOURSPERDAY
|
||||
interest_mode=InterestMode.HOURSPERDAY,
|
||||
trading_mode=TradingMode.MARGIN
|
||||
)
|
||||
|
||||
# 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,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
exchange='binance',
|
||||
exchange='binance'
|
||||
)
|
||||
assert trade.open_order_id 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,
|
||||
leverage=3.0,
|
||||
interest_rate=0.0005,
|
||||
interest_mode=InterestMode.HOURSPERDAY
|
||||
interest_mode=InterestMode.HOURSPERDAY,
|
||||
trading_mode=TradingMode.MARGIN
|
||||
)
|
||||
trade.open_order_id = 'something'
|
||||
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 trade.calc_profit() == 5.685
|
||||
assert trade.calc_profit_ratio() == round(0.0945137157107232, 8)
|
||||
|
||||
# 3x leverage, binance
|
||||
trade.trading_mode = TradingMode.MARGIN
|
||||
trade.leverage = 3
|
||||
trade.interest_mode = InterestMode.HOURSPERDAY
|
||||
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
|
||||
assert trade._calc_open_trade_value() == 60.15
|
||||
|
||||
# Margin
|
||||
trade.trading_mode = TradingMode.MARGIN
|
||||
trade.is_short = True
|
||||
trade.recalc_open_trade_value()
|
||||
assert trade._calc_open_trade_value() == 59.85
|
||||
|
||||
# 3x short margin leverage
|
||||
trade.leverage = 3
|
||||
trade.interest_mode = InterestMode.HOURSPERDAY
|
||||
assert trade._calc_open_trade_value() == 59.85
|
||||
|
||||
# 3x long margin leverage
|
||||
trade.is_short = False
|
||||
trade.recalc_open_trade_value()
|
||||
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
|
||||
|
||||
# 3x leverage binance
|
||||
trade.trading_mode = TradingMode.MARGIN
|
||||
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, 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 = trade._calc_open_trade_value()
|
||||
|
||||
# Margin
|
||||
trade.trading_mode = TradingMode.MARGIN
|
||||
# 3x leverage, long ###################################################
|
||||
trade.leverage = 3.0
|
||||
# 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
|
||||
trade.open_trade_value = trade._calc_open_trade_value()
|
||||
|
||||
# Margin
|
||||
trade.trading_mode = TradingMode.MARGIN
|
||||
# 3x leverage, long ###################################################
|
||||
trade.leverage = 3.0
|
||||
# 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
|
||||
# Initial is true but stop_loss set - so doesn't do anything
|
||||
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_pct == 0.05
|
||||
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)
|
||||
assert trade.stop_loss == 0.63
|
||||
assert trade.isolated_liq == 0.63
|
||||
@ -1707,10 +1723,15 @@ def test_to_json(default_conf, fee):
|
||||
'buy_tag': None,
|
||||
'timeframe': None,
|
||||
'exchange': 'binance',
|
||||
'leverage': None,
|
||||
'interest_rate': None,
|
||||
'trading_mode': None,
|
||||
'isolated_liq': 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
|
||||
@ -1778,10 +1799,15 @@ def test_to_json(default_conf, fee):
|
||||
'buy_tag': 'buys_signal_001',
|
||||
'timeframe': None,
|
||||
'exchange': 'binance',
|
||||
'leverage': None,
|
||||
'interest_rate': None,
|
||||
'trading_mode': None,
|
||||
'isolated_liq': 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_pct == 0.04
|
||||
# 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)
|
||||
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_order_trades',
|
||||
'get_trades',
|
||||
'last_funding_adjustment'
|
||||
)
|
||||
|
||||
# Parent (LocalTrade) should have the same attributes
|
||||
|
Loading…
Reference in New Issue
Block a user