Created FundingFee class and added funding_fee to LocalTrade and freqtradebot
This commit is contained in:
parent
5184cc7749
commit
b7891485b3
@ -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.funding_fee 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
|
||||||
@ -559,6 +565,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(
|
||||||
@ -576,10 +586,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)
|
||||||
|
@ -1,2 +1 @@
|
|||||||
# flake8: noqa: F401
|
# flake8: noqa: F401
|
||||||
from freqtrade.leverage.interest import interest
|
|
||||||
|
80
freqtrade/leverage/funding_fee.py
Normal file
80
freqtrade/leverage/funding_fee.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import schedule
|
||||||
|
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
|
class FundingFee:
|
||||||
|
|
||||||
|
trades: List[Trade]
|
||||||
|
# Binance
|
||||||
|
begin_times = [
|
||||||
|
# TODO-lev: Make these UTC time
|
||||||
|
"23:59:45",
|
||||||
|
"07:59:45",
|
||||||
|
"15:59:45",
|
||||||
|
]
|
||||||
|
|
||||||
|
# FTX
|
||||||
|
# begin_times = every hour
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# FTX - paid in USD(always)
|
||||||
|
# position size * TWAP of((future - index) / index) / 24
|
||||||
|
# https: // help.ftx.com/hc/en-us/articles/360027946571-Funding
|
||||||
|
return
|
||||||
|
|
||||||
|
def initial_funding_fee(self, amount) -> float:
|
||||||
|
# A funding fee interval is applied immediately if within 30s of an iterval
|
||||||
|
# May only exist on binance
|
||||||
|
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,11 +49,21 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
|
|||||||
strategy = get_column_def(cols, 'strategy', 'null')
|
strategy = get_column_def(cols, 'strategy', 'null')
|
||||||
buy_tag = get_column_def(cols, 'buy_tag', 'null')
|
buy_tag = get_column_def(cols, 'buy_tag', 'null')
|
||||||
|
|
||||||
|
trading_mode = get_column_def(cols, 'trading_mode', 'null')
|
||||||
|
|
||||||
|
# Leverage Properties
|
||||||
leverage = get_column_def(cols, 'leverage', '1.0')
|
leverage = get_column_def(cols, 'leverage', '1.0')
|
||||||
interest_rate = get_column_def(cols, 'interest_rate', '0.0')
|
|
||||||
isolated_liq = get_column_def(cols, 'isolated_liq', 'null')
|
isolated_liq = get_column_def(cols, 'isolated_liq', 'null')
|
||||||
# sqlite does not support literals for booleans
|
# sqlite does not support literals for booleans
|
||||||
is_short = get_column_def(cols, 'is_short', '0')
|
is_short = get_column_def(cols, 'is_short', '0')
|
||||||
|
|
||||||
|
# Margin Properties
|
||||||
|
interest_rate = get_column_def(cols, 'interest_rate', '0.0')
|
||||||
|
|
||||||
|
# Futures properties
|
||||||
|
funding_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')
|
||||||
@ -91,7 +101,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
|
|||||||
stoploss_order_id, stoploss_last_update,
|
stoploss_order_id, stoploss_last_update,
|
||||||
max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag,
|
max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag,
|
||||||
timeframe, open_trade_value, close_profit_abs,
|
timeframe, open_trade_value, close_profit_abs,
|
||||||
leverage, interest_rate, isolated_liq, is_short
|
trading_mode, leverage, isolated_liq, is_short,
|
||||||
|
interest_rate, funding_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,
|
||||||
@ -108,8 +119,9 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
|
|||||||
{sell_order_status} sell_order_status,
|
{sell_order_status} sell_order_status,
|
||||||
{strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe,
|
{strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe,
|
||||||
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
|
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
|
||||||
{leverage} leverage, {interest_rate} interest_rate,
|
{trading_mode} trading_mode, {leverage} leverage, {isolated_liq} isolated_liq,
|
||||||
{isolated_liq} isolated_liq, {is_short} is_short
|
{is_short} is_short, {interest_rate} interest_rate,
|
||||||
|
{funding_fee} funding_fee, {last_funding_adjustment} last_funding_adjustment
|
||||||
from {table_back_name}
|
from {table_back_name}
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
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
|
||||||
|
|
||||||
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String,
|
from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
|
||||||
create_engine, desc, func, inspect)
|
create_engine, desc, func, inspect)
|
||||||
from sqlalchemy.exc import NoSuchModuleError
|
from sqlalchemy.exc import NoSuchModuleError
|
||||||
from sqlalchemy.orm import Query, declarative_base, relationship, scoped_session, sessionmaker
|
from sqlalchemy.orm import Query, declarative_base, relationship, scoped_session, sessionmaker
|
||||||
@ -14,9 +14,9 @@ from sqlalchemy.pool import StaticPool
|
|||||||
from sqlalchemy.sql.schema import UniqueConstraint
|
from sqlalchemy.sql.schema import UniqueConstraint
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES
|
||||||
from freqtrade.enums import SellType
|
from freqtrade.enums import SellType, TradingMode
|
||||||
from freqtrade.exceptions import DependencyException, OperationalException
|
from freqtrade.exceptions import DependencyException, OperationalException
|
||||||
from freqtrade.leverage import interest
|
from freqtrade.leverage.interest import interest
|
||||||
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
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None:
|
|||||||
f"is no valid database URL! (See {_SQL_DOCS_URL})")
|
f"is no valid database URL! (See {_SQL_DOCS_URL})")
|
||||||
|
|
||||||
# https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope
|
# https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope
|
||||||
# Scoped sessions proxy requests to the appropriate thread-local session.
|
# Scoped sessions proxy reque sts to the appropriate thread-local session.
|
||||||
# We should use the scoped_session object - not a seperately initialized version
|
# We should use the scoped_session object - not a seperately initialized version
|
||||||
Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=True))
|
Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=True))
|
||||||
Trade.query = Trade._session.query_property()
|
Trade.query = Trade._session.query_property()
|
||||||
@ -93,6 +93,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
|
||||||
@ -265,14 +271,20 @@ class LocalTrade():
|
|||||||
buy_tag: Optional[str] = None
|
buy_tag: Optional[str] = None
|
||||||
timeframe: Optional[int] = None
|
timeframe: Optional[int] = None
|
||||||
|
|
||||||
|
trading_mode: TradingMode = TradingMode.SPOT
|
||||||
|
|
||||||
# Leverage trading properties
|
# Leverage trading properties
|
||||||
is_short: bool = False
|
|
||||||
isolated_liq: Optional[float] = None
|
isolated_liq: Optional[float] = None
|
||||||
|
is_short: bool = False
|
||||||
leverage: float = 1.0
|
leverage: float = 1.0
|
||||||
|
|
||||||
# Margin trading properties
|
# Margin trading properties
|
||||||
interest_rate: float = 0.0
|
interest_rate: float = 0.0
|
||||||
|
|
||||||
|
# Futures properties
|
||||||
|
funding_fee: Optional[float] = None
|
||||||
|
last_funding_adjustment: Optional[datetime] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_no_leverage(self) -> bool:
|
def has_no_leverage(self) -> bool:
|
||||||
"""Returns true if this is a non-leverage, non-short trade"""
|
"""Returns true if this is a non-leverage, non-short trade"""
|
||||||
@ -438,7 +450,10 @@ class LocalTrade():
|
|||||||
'interest_rate': self.interest_rate,
|
'interest_rate': self.interest_rate,
|
||||||
'isolated_liq': self.isolated_liq,
|
'isolated_liq': self.isolated_liq,
|
||||||
'is_short': self.is_short,
|
'is_short': self.is_short,
|
||||||
|
'trading_mode': self.trading_mode,
|
||||||
|
'funding_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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -516,6 +531,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.
|
||||||
@ -654,8 +673,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 maybe
|
||||||
return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
|
return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
|
||||||
|
|
||||||
|
def _calc_base_close(self, amount: Decimal, rate: Optional[float] = None,
|
||||||
|
fee: Optional[float] = None) -> Decimal:
|
||||||
|
|
||||||
|
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
|
||||||
|
fees = close_trade * Decimal(fee or self.fee_close)
|
||||||
|
|
||||||
|
if self.is_short:
|
||||||
|
return close_trade + fees
|
||||||
|
else:
|
||||||
|
return close_trade - fees
|
||||||
|
|
||||||
def calc_close_trade_value(self, rate: Optional[float] = None,
|
def calc_close_trade_value(self, rate: Optional[float] = None,
|
||||||
fee: Optional[float] = None,
|
fee: Optional[float] = None,
|
||||||
interest_rate: Optional[float] = None) -> float:
|
interest_rate: Optional[float] = None) -> float:
|
||||||
@ -672,20 +703,32 @@ class LocalTrade():
|
|||||||
if rate is None and not self.close_rate:
|
if rate is None and not self.close_rate:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
interest = self.calculate_interest(interest_rate)
|
amount = Decimal(self.amount)
|
||||||
if self.is_short:
|
trading_mode = self.trading_mode or TradingMode.SPOT
|
||||||
amount = Decimal(self.amount) + Decimal(interest)
|
|
||||||
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
|
if trading_mode == TradingMode.SPOT:
|
||||||
fees = close_trade * Decimal(fee or self.fee_close)
|
return float(self._calc_base_close(amount, rate, fee))
|
||||||
|
|
||||||
if self.is_short:
|
elif (trading_mode == TradingMode.MARGIN):
|
||||||
return float(close_trade + fees)
|
|
||||||
|
total_interest = self.calculate_interest(interest_rate)
|
||||||
|
|
||||||
|
if self.is_short:
|
||||||
|
amount = amount + total_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) - total_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:
|
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,
|
def calc_profit(self, rate: Optional[float] = None,
|
||||||
fee: Optional[float] = None,
|
fee: Optional[float] = None,
|
||||||
@ -893,14 +936,19 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||||||
buy_tag = Column(String(100), nullable=True)
|
buy_tag = Column(String(100), nullable=True)
|
||||||
timeframe = Column(Integer, nullable=True)
|
timeframe = Column(Integer, nullable=True)
|
||||||
|
|
||||||
# Leverage trading properties
|
trading_mode = Column(Enum(TradingMode))
|
||||||
leverage = Column(Float, nullable=True, default=1.0)
|
|
||||||
is_short = Column(Boolean, nullable=False, default=False)
|
|
||||||
isolated_liq = Column(Float, nullable=True)
|
|
||||||
|
|
||||||
# Margin Trading Properties
|
leverage = Column(Float, nullable=True, default=1.0)
|
||||||
|
isolated_liq = Column(Float, nullable=True)
|
||||||
|
is_short = Column(Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
|
# Margin properties
|
||||||
interest_rate = Column(Float, nullable=False, default=0.0)
|
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)
|
||||||
self.recalc_open_trade_value()
|
self.recalc_open_trade_value()
|
||||||
|
@ -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.20
|
prompt-toolkit==3.0.20
|
||||||
|
|
||||||
|
#Futures
|
||||||
|
schedule==1.1.0
|
||||||
|
@ -3,7 +3,7 @@ from math import isclose
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.leverage import interest
|
from freqtrade.leverage.interest import interest
|
||||||
|
|
||||||
|
|
||||||
ten_mins = Decimal(1/6)
|
ten_mins = Decimal(1/6)
|
||||||
|
@ -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,13 @@ 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,
|
||||||
|
'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 +182,13 @@ 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,
|
||||||
|
'interest_rate': 0.0,
|
||||||
|
'funding_fee': None,
|
||||||
|
'last_funding_adjustment': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,6 +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 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
|
||||||
@ -90,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',
|
||||||
@ -236,6 +237,7 @@ def test_interest(market_buy_order_usdt, fee):
|
|||||||
exchange='binance',
|
exchange='binance',
|
||||||
leverage=3.0,
|
leverage=3.0,
|
||||||
interest_rate=0.0005,
|
interest_rate=0.0005,
|
||||||
|
trading_mode=TradingMode.MARGIN
|
||||||
)
|
)
|
||||||
|
|
||||||
# 10min, 3x leverage
|
# 10min, 3x leverage
|
||||||
@ -548,6 +550,7 @@ 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,
|
||||||
|
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)
|
||||||
@ -639,6 +642,7 @@ def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt
|
|||||||
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.exchange = "binance"
|
trade.exchange = "binance"
|
||||||
assert trade._calc_open_trade_value() == 60.15
|
assert trade._calc_open_trade_value() == 60.15
|
||||||
@ -796,12 +800,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.exchange = "binance"
|
trade.exchange = "binance"
|
||||||
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
|
||||||
@ -838,6 +849,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
|
||||||
@ -1037,6 +1049,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
|
||||||
@ -1139,6 +1153,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
|
||||||
@ -1707,6 +1723,9 @@ def test_to_json(default_conf, fee):
|
|||||||
'interest_rate': None,
|
'interest_rate': None,
|
||||||
'isolated_liq': None,
|
'isolated_liq': None,
|
||||||
'is_short': None,
|
'is_short': None,
|
||||||
|
'trading_mode': None,
|
||||||
|
'funding_fee': None,
|
||||||
|
'last_funding_adjustment': None
|
||||||
}
|
}
|
||||||
|
|
||||||
# Simulate dry_run entries
|
# Simulate dry_run entries
|
||||||
@ -1778,6 +1797,9 @@ def test_to_json(default_conf, fee):
|
|||||||
'interest_rate': None,
|
'interest_rate': None,
|
||||||
'isolated_liq': None,
|
'isolated_liq': None,
|
||||||
'is_short': None,
|
'is_short': None,
|
||||||
|
'trading_mode': None,
|
||||||
|
'funding_fee': None,
|
||||||
|
'last_funding_adjustment': None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -2197,6 +2219,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
|
||||||
|
Loading…
Reference in New Issue
Block a user