Wrote buy/short adjustments in freqtradebot and exchange

This commit is contained in:
Sam Germain 2021-07-25 23:40:38 -06:00
parent 3671a8aa13
commit 051bdc5fd0
8 changed files with 98 additions and 35 deletions

View File

@ -1,9 +1,11 @@
# from decimal import Decimal # 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 enum import Enum
from freqtrade.enums.collateral import Collateral
from freqtrade.enums.tradingmode import TradingMode
from freqtrade.exceptions import OperationalException
# from math import ceil # from math import ceil

View File

@ -4,7 +4,7 @@ from typing import Dict, Optional
import ccxt import ccxt
from freqtrade.enums import LiqFormula from freqtrade.enums import InterestMode, LiqFormula
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError) OperationalException, TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -24,6 +24,7 @@ class Binance(Exchange):
"trades_pagination_arg": "fromId", "trades_pagination_arg": "fromId",
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
} }
interest_mode: InterestMode = InterestMode.HOURSPERDAY
maintenance_margin_formula = LiqFormula.BINANCE maintenance_margin_formula = LiqFormula.BINANCE
@ -193,3 +194,8 @@ class Binance(Exchange):
def apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): def apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float):
return stake_amount / leverage 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

View File

@ -2,8 +2,8 @@
import logging import logging
from typing import Dict, Optional from typing import Dict, Optional
from freqtrade.exchange import Exchange
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -44,3 +44,8 @@ class Bittrex(Exchange):
is_short: Optional[bool] is_short: Optional[bool]
): ):
raise OperationalException("Bittrex does not support leveraged trading") 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")

View File

@ -21,7 +21,7 @@ from pandas import DataFrame
from freqtrade.constants import DEFAULT_AMOUNT_RESERVE_PERCENT, ListPairsWithTimeframes from freqtrade.constants import DEFAULT_AMOUNT_RESERVE_PERCENT, ListPairsWithTimeframes
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list 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, from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
InvalidOrderException, OperationalException, PricingError, InvalidOrderException, OperationalException, PricingError,
RetryableOrderError, TemporaryError) RetryableOrderError, TemporaryError)
@ -70,6 +70,7 @@ class Exchange:
} }
_ft_has: Dict = {} _ft_has: Dict = {}
liq_formula: LiqFormula liq_formula: LiqFormula
interest_mode: InterestMode = InterestMode.NONE
def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: def __init__(self, config: Dict[str, Any], validate: bool = True) -> None:
""" """
@ -715,7 +716,7 @@ class Exchange:
# Order handling # Order handling
def create_order(self, pair: str, ordertype: str, side: str, amount: float, 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']: if self._config['dry_run']:
dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate) dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate)
@ -1533,7 +1534,7 @@ class Exchange:
:returns List of trade data :returns List of trade data
""" """
if not self.exchange_has("fetchTrades"): 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( return asyncio.get_event_loop().run_until_complete(
self._async_get_trade_history(pair=pair, since=since, 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]): def transfer(self, asset: str, amount: float, frm: str, to: str, pair: Optional[str]):
self._api.transfer(asset, amount, frm, to) 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: def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
return exchange_name in ccxt_exchanges(ccxt_module) return exchange_name in ccxt_exchanges(ccxt_module)

View File

@ -155,3 +155,9 @@ class Ftx(Exchange):
if order['type'] == 'stop': if order['type'] == 'stop':
return safe_value_fallback2(order, order, 'id_stop', 'id') return safe_value_fallback2(order, order, 'id_stop', 'id')
return order['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?")

View File

@ -4,7 +4,7 @@ from typing import Any, Dict, Optional
import ccxt import ccxt
from freqtrade.enums import LiqFormula from freqtrade.enums import InterestMode, LiqFormula
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError) OperationalException, TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -23,6 +23,7 @@ class Kraken(Exchange):
"trades_pagination": "id", "trades_pagination": "id",
"trades_pagination_arg": "since", "trades_pagination_arg": "since",
} }
interest_mode: InterestMode = InterestMode.HOURSPER4
maintenance_margin_formula = LiqFormula.KRAKEN maintenance_margin_formula = LiqFormula.KRAKEN
@ -147,3 +148,8 @@ class Kraken(Exchange):
is_short: Optional[bool] is_short: Optional[bool]
): ):
return 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")

View File

@ -16,7 +16,7 @@ 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 InterestMode, RPCMessageType, SellType, State
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
@ -442,8 +442,8 @@ class FreqtradeBot(LoggingMixin):
logger.debug(f"Can't open a new trade for {pair}: max number of trades is reached.") logger.debug(f"Can't open a new trade for {pair}: max number of trades is reached.")
return False return False
long_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 = 3 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 # running get_signal on historical data fetched
(buy, sell, buy_tag) = self.strategy.get_signal( (buy, sell, buy_tag) = self.strategy.get_signal(
pair, pair,
@ -528,7 +528,7 @@ class FreqtradeBot(LoggingMixin):
stake_amount: float, stake_amount: float,
price: Optional[float] = None, price: Optional[float] = None,
forcebuy: bool = False, forcebuy: bool = False,
leverage: Optional[float] = 1.0, leverage: float = 1.0,
is_short: bool = False, is_short: bool = False,
buy_tag: Optional[str] = None buy_tag: Optional[str] = None
) -> bool: ) -> bool:
@ -540,16 +540,17 @@ class FreqtradeBot(LoggingMixin):
:return: True if a buy order is created, false if it fails. :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 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: if price:
enter_limit_requested = price enter_limit_requested = price
else: else:
# Calculate price # 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: 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( min_stake_amount = self.exchange.get_min_pair_stake_amount(
pair=pair, pair=pair,
@ -570,61 +571,82 @@ class FreqtradeBot(LoggingMixin):
if not stake_amount: if not stake_amount:
return False 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: " logger.info(f"{log_type}: about create a new trade for {pair} with stake_amount: "
f"{stake_amount} ...") f"{stake_amount} ...")
amount = stake_amount / enter_limit_requested amount = (stake_amount / enter_limit_requested) * leverage
order_type = self.strategy.order_types[trade_type] order_type = self.strategy.order_types[side] # TODO-mg: Don't knoww what to do here
if forcebuy: if forcebuy:
# Forcebuy can define a different ordertype # 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) 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)( 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, pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
time_in_force=time_in_force, current_time=datetime.now(timezone.utc)): 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 return False
amount = self.exchange.amount_to_precision(pair, amount) 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, amount=amount, rate=enter_limit_requested,
time_in_force=time_in_force) 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_id = order['id']
order_status = order.get('status', None) order_status = order.get('status', None)
# we assume the order is executed at the price requested # 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 amount_requested = amount
if order_status == 'expired' or order_status == 'rejected': 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 # return false if the order is not filled
if float(order['filled']) == 0: 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.', ' 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 return False
else: else:
# the order is partially fulfilled # the order is partially fulfilled
# in case of IOC orders we can check immediately # in case of IOC orders we can check immediately
# if the order is fulfilled fully or partially # 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).', ' %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'] order['filled'], order['amount'], order['remaining']
) )
stake_amount = order['cost'] stake_amount = order['cost']
amount = safe_value_fallback(order, 'filled', 'amount') 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 # in case of FOK the order may be filled immediately and fully
elif order_status == 'closed': elif order_status == 'closed':
stake_amount = order['cost'] stake_amount = order['cost']
amount = safe_value_fallback(order, 'filled', 'amount') 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 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')
@ -636,14 +658,19 @@ class FreqtradeBot(LoggingMixin):
amount_requested=amount_requested, amount_requested=amount_requested,
fee_open=fee, fee_open=fee,
fee_close=fee, fee_close=fee,
open_rate=buy_limit_filled_price, open_rate=enter_limit_filled_price,
open_rate_requested=enter_limit_requested, open_rate_requested=enter_limit_requested,
open_date=datetime.utcnow(), open_date=datetime.utcnow(),
exchange=self.exchange.id, exchange=self.exchange.id,
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']),
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) trade.orders.append(order_obj)

View File

@ -2177,7 +2177,7 @@ def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange
pair = 'ETH/BTC' pair = 'ETH/BTC'
with pytest.raises(OperationalException, 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], exchange.get_historic_trades(pair, since=trades_history[0][0],
until=trades_history[-1][0]) until=trades_history[-1][0])