diff --git a/freqtrade/enums/liqformula.py b/freqtrade/enums/liqformula.py index 158e5f9b3..471a45624 100644 --- a/freqtrade/enums/liqformula.py +++ b/freqtrade/enums/liqformula.py @@ -1,9 +1,11 @@ # from decimal import Decimal -from freqtrade.exceptions import OperationalException -from freqtrade.enums.tradingmode import TradingMode -from freqtrade.enums.collateral import Collateral 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 diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 3a1e466e0..1ca1661c6 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -4,7 +4,7 @@ from typing import Dict, Optional import ccxt -from freqtrade.enums import LiqFormula +from freqtrade.enums import InterestMode, LiqFormula from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -24,6 +24,7 @@ class Binance(Exchange): "trades_pagination_arg": "fromId", "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], } + interest_mode: InterestMode = InterestMode.HOURSPERDAY maintenance_margin_formula = LiqFormula.BINANCE @@ -193,3 +194,8 @@ class Binance(Exchange): def apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): return stake_amount / leverage + + def get_isolated_liq(self, pair: str, open_rate: float, + amount: float, leverage: float, is_short: bool) -> float: + # TODO-mg: implement + return 0.0 diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index e4d344d27..d9dc9287b 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -2,8 +2,8 @@ import logging from typing import Dict, Optional -from freqtrade.exchange import Exchange from freqtrade.exceptions import OperationalException +from freqtrade.exchange import Exchange logger = logging.getLogger(__name__) @@ -44,3 +44,8 @@ class Bittrex(Exchange): is_short: Optional[bool] ): raise OperationalException("Bittrex does not support leveraged trading") + + def get_isolated_liq(self, pair: str, open_rate: float, + amount: float, leverage: float, is_short: bool) -> float: + # TODO-mg: implement + raise OperationalException("Bittrex does not support margin trading") diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 082509cd8..9f5d9a0a6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -21,7 +21,7 @@ from pandas import DataFrame from freqtrade.constants import DEFAULT_AMOUNT_RESERVE_PERCENT, ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list -from freqtrade.enums import LiqFormula +from freqtrade.enums import InterestMode, LiqFormula from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, RetryableOrderError, TemporaryError) @@ -70,6 +70,7 @@ class Exchange: } _ft_has: Dict = {} liq_formula: LiqFormula + interest_mode: InterestMode = InterestMode.NONE def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: """ @@ -570,7 +571,7 @@ class Exchange: def apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): """ - #* Should be implemented by child classes if leverage affects the stake_amount + # * Should be implemented by child classes if leverage affects the stake_amount Takes the minimum stake amount for a pair with no leverage and returns the minimum stake amount when leverage is considered :param stake_amount: The stake amount for a pair before leverage is considered @@ -715,7 +716,7 @@ class Exchange: # Order handling def create_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, time_in_force: str = 'gtc') -> Dict: + rate: float, time_in_force: str = "gtc") -> Dict: if self._config['dry_run']: dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate) @@ -1533,7 +1534,7 @@ class Exchange: :returns List of trade data """ if not self.exchange_has("fetchTrades"): - raise OperationalException("This exchange does not suport downloading Trades.") + raise OperationalException("This exchange does not support downloading Trades.") return asyncio.get_event_loop().run_until_complete( self._async_get_trade_history(pair=pair, since=since, @@ -1542,6 +1543,16 @@ class Exchange: def transfer(self, asset: str, amount: float, frm: str, to: str, pair: Optional[str]): self._api.transfer(asset, amount, frm, to) + def get_isolated_liq(self, pair: str, open_rate: float, + amount: float, leverage: float, is_short: bool) -> float: + raise OperationalException( + f"Isolated margin is not available on {self.name} using freqtrade" + ) + + def get_interest_rate(self, pair: str, open_rate: float, is_short: bool) -> float: + # TODO-mg: implement + return 0.0005 + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index c1aa2bef2..cc090dcfc 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -155,3 +155,9 @@ class Ftx(Exchange): if order['type'] == 'stop': return safe_value_fallback2(order, order, 'id_stop', 'id') return order['id'] + + def get_isolated_liq(self, pair: str, open_rate: float, + amount: float, leverage: float, is_short: bool) -> float: + # TODO-mg: implement + raise OperationalException( + "Isolated margin trading not yet implemented on FTX, would you like to implement it?") diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index f247ca144..904b41512 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -4,7 +4,7 @@ from typing import Any, Dict, Optional import ccxt -from freqtrade.enums import LiqFormula +from freqtrade.enums import InterestMode, LiqFormula from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -23,6 +23,7 @@ class Kraken(Exchange): "trades_pagination": "id", "trades_pagination_arg": "since", } + interest_mode: InterestMode = InterestMode.HOURSPER4 maintenance_margin_formula = LiqFormula.KRAKEN @@ -147,3 +148,8 @@ class Kraken(Exchange): is_short: Optional[bool] ): return + + def get_isolated_liq(self, pair: str, open_rate: float, + amount: float, leverage: float, is_short: bool) -> float: + # TODO-mg: implement + raise OperationalException("Kraken only supports cross margin trading") diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a5d2f381c..2af9ab5ae 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,7 +16,7 @@ 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 InterestMode, RPCMessageType, SellType, State from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds @@ -442,8 +442,8 @@ class FreqtradeBot(LoggingMixin): logger.debug(f"Can't open a new trade for {pair}: max number of trades is reached.") return False - long_lev = 3 if True else 1.0 # Replace with self.strategy.get_leverage - short_lev = 3 if True else 1.0 # Replace with self.strategy.get_leverage + long_lev = 1.0 if True else 1.0 # Replace with self.strategy.get_leverage + short_lev = 1.0 if True else 1.0 # Replace with self.strategy.get_leverage # running get_signal on historical data fetched (buy, sell, buy_tag) = self.strategy.get_signal( pair, @@ -528,7 +528,7 @@ class FreqtradeBot(LoggingMixin): stake_amount: float, price: Optional[float] = None, forcebuy: bool = False, - leverage: Optional[float] = 1.0, + leverage: float = 1.0, is_short: bool = False, buy_tag: Optional[str] = None ) -> bool: @@ -540,16 +540,17 @@ class FreqtradeBot(LoggingMixin): :return: True if a buy order is created, false if it fails. """ time_in_force = self.strategy.order_time_in_force['buy'] # TODO-mg Change to enter - trade_type = 'short' if is_short else 'buy' + side = 'sell' if is_short else 'buy' + name = 'Short' if is_short else 'Buy' if price: enter_limit_requested = price else: # Calculate price - enter_limit_requested = self.exchange.get_rate(pair, refresh=True, side=trade_type) + enter_limit_requested = self.exchange.get_rate(pair, refresh=True, side=side) if not enter_limit_requested: - raise PricingError('Could not determine buy price.') + raise PricingError(f'Could not determine {side} price.') min_stake_amount = self.exchange.get_min_pair_stake_amount( pair=pair, @@ -570,61 +571,82 @@ class FreqtradeBot(LoggingMixin): if not stake_amount: return False - log_type = f"{trade_type.capitalize()} signal found" + log_type = f"{name} signal found" logger.info(f"{log_type}: about create a new trade for {pair} with stake_amount: " f"{stake_amount} ...") - amount = stake_amount / enter_limit_requested - order_type = self.strategy.order_types[trade_type] + amount = (stake_amount / enter_limit_requested) * leverage + order_type = self.strategy.order_types[side] # TODO-mg: Don't knoww what to do here if forcebuy: # Forcebuy can define a different ordertype - # TODO-mg get a forceshort + # TODO-mg get a forceshort? What is this order_type = self.strategy.order_types.get('forcebuy', order_type) + # TODO-mg:Will this work for shorting? if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, time_in_force=time_in_force, current_time=datetime.now(timezone.utc)): - logger.info(f"User requested abortion of buying {pair}") + logger.info(f"User requested abortion of {name.lower()}ing {pair}") return False amount = self.exchange.amount_to_precision(pair, amount) - order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy", + order = self.exchange.create_order(pair=pair, ordertype=order_type, side=side, amount=amount, rate=enter_limit_requested, time_in_force=time_in_force) - order_obj = Order.parse_from_ccxt_object(order, pair, 'buy') + order_obj = Order.parse_from_ccxt_object(order, pair, side) order_id = order['id'] order_status = order.get('status', None) # we assume the order is executed at the price requested - buy_limit_filled_price = enter_limit_requested + enter_limit_filled_price = enter_limit_requested amount_requested = amount if order_status == 'expired' or order_status == 'rejected': - order_tif = self.strategy.order_time_in_force['buy'] + order_tif = self.strategy.order_time_in_force['buy'] # TODO-mg: Update to enter # return false if the order is not filled if float(order['filled']) == 0: - logger.warning('Buy %s order with time in force %s for %s is %s by %s.' + logger.warning('%s %s order with time in force %s for %s is %s by %s.' ' zero amount is fulfilled.', - order_tif, order_type, pair, order_status, self.exchange.name) + name, order_tif, order_type, pair, order_status, self.exchange.name) return False else: # the order is partially fulfilled # in case of IOC orders we can check immediately # if the order is fulfilled fully or partially - logger.warning('Buy %s order with time in force %s for %s is %s by %s.' + + logger.warning('%s %s order with time in force %s for %s is %s by %s.' ' %s amount fulfilled out of %s (%s remaining which is canceled).', - order_tif, order_type, pair, order_status, self.exchange.name, + name, order_tif, order_type, pair, order_status, self.exchange.name, order['filled'], order['amount'], order['remaining'] ) stake_amount = order['cost'] amount = safe_value_fallback(order, 'filled', 'amount') - buy_limit_filled_price = safe_value_fallback(order, 'average', 'price') + enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') # in case of FOK the order may be filled immediately and fully elif order_status == 'closed': stake_amount = order['cost'] amount = safe_value_fallback(order, 'filled', 'amount') - buy_limit_filled_price = safe_value_fallback(order, 'average', 'price') + enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') + + interest_rate = 0 + isolated_liq = None + + if leverage > 1.0: # TODO-mg: and margin == isolated: + isolated_liq = self.exchange.get_isolated_liq( + pair=pair, + open_rate=enter_limit_filled_price, + amount=amount, + leverage=leverage, + is_short=is_short + ) + + if leverage > 1.0: + interest_rate = self.exchange.get_interest_rate( + pair=pair, + open_rate=enter_limit_filled_price, + is_short=is_short + ) # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') @@ -636,14 +658,19 @@ class FreqtradeBot(LoggingMixin): amount_requested=amount_requested, fee_open=fee, fee_close=fee, - open_rate=buy_limit_filled_price, + open_rate=enter_limit_filled_price, open_rate_requested=enter_limit_requested, open_date=datetime.utcnow(), exchange=self.exchange.id, 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']), + leverage=leverage, + is_short=is_short, + interest_rate=interest_rate, + interest_mode=self.exchange.interest_mode, + isolated_liq=isolated_liq ) trade.orders.append(order_obj) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a3ebbe8bd..935775477 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2177,7 +2177,7 @@ def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange pair = 'ETH/BTC' with pytest.raises(OperationalException, - match="This exchange does not suport downloading Trades."): + match="This exchange does not support downloading Trades."): exchange.get_historic_trades(pair, since=trades_history[0][0], until=trades_history[-1][0])