From e3845ff808dd180db478937898c87c3e6a4d2ad5 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 3 Aug 2021 12:55:22 -0600 Subject: [PATCH] Created FundingFee class and added funding_fee to LocalTrade and freqtradebot --- freqtrade/enums/__init__.py | 3 + freqtrade/enums/collateral.py | 11 +++ freqtrade/enums/liqformula.py | 108 ++++++++++++++++++++++++ freqtrade/enums/tradingmode.py | 11 +++ freqtrade/freqtradebot.py | 19 ++++- freqtrade/leverage/__init__.py | 2 + freqtrade/leverage/funding_fee.py | 71 ++++++++++++++++ freqtrade/persistence/migrations.py | 24 ++++-- freqtrade/persistence/models.py | 126 +++++++++++++++++++++------- requirements.txt | 3 + tests/rpc/test_rpc.py | 20 +++-- tests/test_persistence.py | 63 ++++++++++---- 12 files changed, 397 insertions(+), 64 deletions(-) create mode 100644 freqtrade/enums/collateral.py create mode 100644 freqtrade/enums/liqformula.py create mode 100644 freqtrade/enums/tradingmode.py create mode 100644 freqtrade/leverage/__init__.py create mode 100644 freqtrade/leverage/funding_fee.py diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index 6099f7003..8398c91f1 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -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 diff --git a/freqtrade/enums/collateral.py b/freqtrade/enums/collateral.py new file mode 100644 index 000000000..0a5988698 --- /dev/null +++ b/freqtrade/enums/collateral.py @@ -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" diff --git a/freqtrade/enums/liqformula.py b/freqtrade/enums/liqformula.py new file mode 100644 index 000000000..471a45624 --- /dev/null +++ b/freqtrade/enums/liqformula.py @@ -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) diff --git a/freqtrade/enums/tradingmode.py b/freqtrade/enums/tradingmode.py new file mode 100644 index 000000000..a8de60c19 --- /dev/null +++ b/freqtrade/enums/tradingmode.py @@ -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" diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 179c99d2c..88a8a5f53 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -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) diff --git a/freqtrade/leverage/__init__.py b/freqtrade/leverage/__init__.py new file mode 100644 index 000000000..77524a4bb --- /dev/null +++ b/freqtrade/leverage/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa: F401 +from freqtrade.leverage.funding_fee import FundingFee diff --git a/freqtrade/leverage/funding_fee.py b/freqtrade/leverage/funding_fee.py new file mode 100644 index 000000000..1f41b3ab6 --- /dev/null +++ b/freqtrade/leverage/funding_fee.py @@ -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) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 03f412724..d12b32d4c 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -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! diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index d09c5ed68..c4108b571 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -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) diff --git a/requirements.txt b/requirements.txt index 6c2ef56c3..c33ba8ded 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index db7ad484c..9f45fdeb0 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -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, } diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 16469f6fc..04f48c6a5 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -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