finished adding TODOs to freqtradebot

This commit is contained in:
Sam Germain 2021-07-24 01:32:42 -06:00
parent 13c8ee9371
commit 3671a8aa13
11 changed files with 412 additions and 150 deletions

View File

@ -5,13 +5,23 @@ class RPCMessageType(Enum):
STATUS = 'status' STATUS = 'status'
WARNING = 'warning' WARNING = 'warning'
STARTUP = 'startup' STARTUP = 'startup'
BUY = 'buy' BUY = 'buy'
BUY_FILL = 'buy_fill' BUY_FILL = 'buy_fill'
BUY_CANCEL = 'buy_cancel' BUY_CANCEL = 'buy_cancel'
SELL = 'sell' SELL = 'sell'
SELL_FILL = 'sell_fill' SELL_FILL = 'sell_fill'
SELL_CANCEL = 'sell_cancel' SELL_CANCEL = 'sell_cancel'
SHORT = 'short'
SHORT_FILL = 'short_fill'
SHORT_CANCEL = 'short_cancel'
EXIT_SHORT = 'exit_short'
EXIT_SHORT_FILL = 'exit_short_fill'
EXIT_SHORT_CANCEL = 'exit_short_cancel'
def __repr__(self): def __repr__(self):
return self.value return self.value

View File

@ -7,6 +7,8 @@ class SignalType(Enum):
""" """
BUY = "buy" BUY = "buy"
SELL = "sell" SELL = "sell"
SHORT = "short"
EXIT_SHORT = "exit_short"
class SignalTagType(Enum): class SignalTagType(Enum):

View File

@ -1,6 +1,6 @@
""" Binance exchange subclass """ """ Binance exchange subclass """
import logging import logging
from typing import Dict from typing import Dict, Optional
import ccxt import ccxt
@ -92,3 +92,104 @@ class Binance(Exchange):
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def transfer(self, asset: str, amount: float, frm: str, to: str, pair: Optional[str]):
res = self._api.sapi_post_margin_isolated_transfer({
"asset": asset,
"amount": amount,
"transFrom": frm,
"transTo": to,
"symbol": pair
})
logger.info(f"Transfer response: {res}")
def borrow(self, asset: str, amount: float, pair: str):
res = self._api.sapi_post_margin_loan({
"asset": asset,
"isIsolated": True,
"symbol": pair,
"amount": amount
}) # borrow from binance
logger.info(f"Borrow response: {res}")
def repay(self, asset: str, amount: float, pair: str):
res = self._api.sapi_post_margin_repay({
"asset": asset,
"isIsolated": True,
"symbol": pair,
"amount": amount
}) # borrow from binance
logger.info(f"Borrow response: {res}")
def setup_leveraged_enter(
self,
pair: str,
leverage: float,
amount: float,
quote_currency: Optional[str],
is_short: Optional[bool]
):
if not quote_currency or not is_short:
raise OperationalException(
"quote_currency and is_short are required arguments to setup_leveraged_enter"
" when trading with leverage on binance"
)
open_rate = 2 # TODO-mg: get the real open_rate, or real stake_amount
stake_amount = amount * open_rate
if is_short:
borrowed = stake_amount * ((leverage-1)/leverage)
else:
borrowed = amount
self.transfer( # Transfer to isolated margin
asset=quote_currency,
amount=stake_amount,
frm='SPOT',
to='ISOLATED_MARGIN',
pair=pair
)
self.borrow(
asset=quote_currency,
amount=borrowed,
pair=pair
) # borrow from binance
def complete_leveraged_exit(
self,
pair: str,
leverage: float,
amount: float,
quote_currency: Optional[str],
is_short: Optional[bool]
):
if not quote_currency or not is_short:
raise OperationalException(
"quote_currency and is_short are required arguments to setup_leveraged_enter"
" when trading with leverage on binance"
)
open_rate = 2 # TODO-mg: get the real open_rate, or real stake_amount
stake_amount = amount * open_rate
if is_short:
borrowed = stake_amount * ((leverage-1)/leverage)
else:
borrowed = amount
self.repay(
asset=quote_currency,
amount=borrowed,
pair=pair
) # repay binance
self.transfer( # Transfer to isolated margin
asset=quote_currency,
amount=stake_amount,
frm='ISOLATED_MARGIN',
to='SPOT',
pair=pair
)
def apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float):
return stake_amount / leverage

View File

@ -1,8 +1,9 @@
""" Bittrex exchange subclass """ """ Bittrex exchange subclass """
import logging import logging
from typing import Dict from typing import Dict, Optional
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -23,3 +24,23 @@ class Bittrex(Exchange):
}, },
"l2_limit_range": [1, 25, 500], "l2_limit_range": [1, 25, 500],
} }
def setup_leveraged_enter(
self,
pair: str,
leverage: float,
amount: float,
quote_currency: Optional[str],
is_short: Optional[bool]
):
raise OperationalException("Bittrex does not support leveraged trading")
def complete_leveraged_exit(
self,
pair: str,
leverage: float,
amount: float,
quote_currency: Optional[str],
is_short: Optional[bool]
):
raise OperationalException("Bittrex does not support leveraged trading")

View File

@ -186,6 +186,7 @@ class Exchange:
'secret': exchange_config.get('secret'), 'secret': exchange_config.get('secret'),
'password': exchange_config.get('password'), 'password': exchange_config.get('password'),
'uid': exchange_config.get('uid', ''), 'uid': exchange_config.get('uid', ''),
'options': exchange_config.get('options', {})
} }
if ccxt_kwargs: if ccxt_kwargs:
logger.info('Applying additional ccxt config: %s', ccxt_kwargs) logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
@ -526,8 +527,9 @@ class Exchange:
else: else:
return 1 / pow(10, precision) return 1 / pow(10, precision)
def get_min_pair_stake_amount(self, pair: str, price: float, def get_min_pair_stake_amount(self, pair: str, price: float, stoploss: float,
stoploss: float) -> Optional[float]: leverage: Optional[float] = 1.0) -> Optional[float]:
# TODO-mg: Using leverage makes the min stake amount lower (on binance at least)
try: try:
market = self.markets[pair] market = self.markets[pair]
except KeyError: except KeyError:
@ -561,7 +563,20 @@ class Exchange:
# The value returned should satisfy both limits: for amount (base currency) and # The value returned should satisfy both limits: for amount (base currency) and
# for cost (quote, stake currency), so max() is used here. # for cost (quote, stake currency), so max() is used here.
# See also #2575 at github. # See also #2575 at github.
return max(min_stake_amounts) * amount_reserve_percent return self.apply_leverage_to_stake_amount(
max(min_stake_amounts) * amount_reserve_percent,
leverage or 1.0
)
def apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float):
"""
#* 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
:param leverage: The amount of leverage being used on the current trade
"""
return stake_amount
# Dry-run methods # Dry-run methods
@ -688,6 +703,15 @@ class Exchange:
raise InvalidOrderException( raise InvalidOrderException(
f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
def get_max_leverage(self, pair: str, stake_amount: float, price: float) -> float:
"""
Gets the maximum leverage available on this pair that is below the config leverage
but higher than the config min_leverage
"""
raise OperationalException(f"Leverage is not available on {self.name} using freqtrade")
return 1.0
# 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,
@ -711,6 +735,7 @@ class Exchange:
order = self._api.create_order(pair, ordertype, side, order = self._api.create_order(pair, ordertype, side,
amount, rate_for_order, params) amount, rate_for_order, params)
self._log_exchange_response('create_order', order) self._log_exchange_response('create_order', order)
return order return order
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
@ -731,6 +756,26 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def setup_leveraged_enter(
self,
pair: str,
leverage: float,
amount: float,
quote_currency: Optional[str],
is_short: Optional[bool]
):
raise OperationalException(f"Leverage is not available on {self.name} using freqtrade")
def complete_leveraged_exit(
self,
pair: str,
leverage: float,
amount: float,
quote_currency: Optional[str],
is_short: Optional[bool]
):
raise OperationalException(f"Leverage is not available on {self.name} using freqtrade")
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
""" """
Verify stop_loss against stoploss-order value (limit or price) Verify stop_loss against stoploss-order value (limit or price)
@ -1494,6 +1539,9 @@ class Exchange:
self._async_get_trade_history(pair=pair, since=since, self._async_get_trade_history(pair=pair, since=since,
until=until, from_id=from_id)) until=until, from_id=from_id))
def transfer(self, asset: str, amount: float, frm: str, to: str, pair: Optional[str]):
self._api.transfer(asset, amount, frm, to)
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

@ -1,6 +1,6 @@
""" Kraken exchange subclass """ """ Kraken exchange subclass """
import logging import logging
from typing import Any, Dict from typing import Any, Dict, Optional
import ccxt import ccxt
@ -127,3 +127,23 @@ class Kraken(Exchange):
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def setup_leveraged_enter(
self,
pair: str,
leverage: float,
amount: float,
quote_currency: Optional[str],
is_short: Optional[bool]
):
return
def complete_leveraged_exit(
self,
pair: str,
leverage: float,
amount: float,
quote_currency: Optional[str],
is_short: Optional[bool]
):
return

View File

@ -412,6 +412,8 @@ class FreqtradeBot(LoggingMixin):
def create_trade(self, pair: str) -> bool: def create_trade(self, pair: str) -> bool:
""" """
# TODO-mg: Just make this function work for shorting and leverage, my todo notes in
# TODO-mg: it aren't very clear
Check the implemented trading strategy for enter signals. Check the implemented trading strategy for enter signals.
If the pair triggers the enter signal a new trade record gets created If the pair triggers the enter signal a new trade record gets created
@ -440,6 +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
short_lev = 3 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,
@ -448,49 +452,45 @@ class FreqtradeBot(LoggingMixin):
) )
(short, exit_short) = (False, False) (short, exit_short) = (False, False)
# = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df) # = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df)
# #TODO: Update strategy class to get these values # TODO-mg: Update strategy class to get these values
open_buy_signal = buy and not sell
open_short_signal = short and not exit_short
# TODO: For a market making strategy, this condition should be removed # TODO: For a market making strategy, this condition should be removed
open_buy_signal = buy and not sell
open_short_signal = short and not exit_short and not open_buy_signal
if (open_buy_signal or open_short_signal): if (open_buy_signal or open_short_signal):
stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
if not stake_amount: if not stake_amount:
logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.")
return False return False
# leverage = 1.0 # TODO: get leverage from somewhere
logger.info( logger.info(
f"{'Buy' if open_buy_signal else 'Short'} signal found: " f"{'Buy' if open_buy_signal else 'Short'} signal found: "
f"about create a new trade for {pair} with stake_amount: " f"about create a new trade for {pair} with stake_amount: "
f"{stake_amount} ..." f"{stake_amount} ..."
) )
# TODO: All later code in this function
if open_buy_signal: if open_buy_signal:
bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) strat = 'bid_strategy'
if ((bid_check_dom.get('enabled', False)) and side = 'buy'
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)): # execute_trade = self.execute_enter
if self._check_depth_of_market_buy(pair, bid_check_dom): lev = long_lev
return self.execute_buy(pair, stake_amount, buy_tag=buy_tag)
else:
return False
else: else:
# TODO-mg: Will check_depth_of_market_buy work for shorts? strat = 'short_strategy'
short_check_dom = self.config.get( side = 'sell'
'short_strategy', {}).get('check_depth_of_market', {}) # execute_trade = self.execute_short
if ((short_check_dom.get('enabled', False)) and lev = short_lev
(short_check_dom.get('shorts_to_ask_delta', 0) > 0)): bid_check_dom = self.config.get(strat, {}).get('check_depth_of_market', {})
if self._check_depth_of_market_short(pair, short_check_dom): if ((bid_check_dom.get('enabled', False)) and
return self.execute_short(pair, stake_amount) (bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
else: if self._check_depth_of_market(pair, bid_check_dom, side):
return False return self.execute_enter(pair, stake_amount, leverage=lev, buy_tag=buy_tag)
else:
return False
return self.execute_buy(pair, stake_amount, buy_tag=buy_tag) return self.execute_enter(pair, stake_amount, leverage=lev, buy_tag=buy_tag)
else: else:
return False return False
def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool: def _check_depth_of_market(self, pair: str, conf: Dict, side: str) -> bool:
""" """
Checks depth of market before executing a buy Checks depth of market before executing a buy
""" """
@ -500,9 +500,17 @@ class FreqtradeBot(LoggingMixin):
order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks']) order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks'])
order_book_bids = order_book_data_frame['b_size'].sum() order_book_bids = order_book_data_frame['b_size'].sum()
order_book_asks = order_book_data_frame['a_size'].sum() order_book_asks = order_book_data_frame['a_size'].sum()
bids_ask_delta = order_book_bids / order_book_asks
enter_side = order_book_bids if side == "buy" else order_book_asks
exit_side = order_book_asks if side == "buy" else order_book_bids
bids_ask_delta = enter_side / exit_side
bids = f"Bids: {order_book_bids}"
asks = f"Asks: {order_book_asks}"
delta = f"Delta: {bids_ask_delta}"
logger.info( logger.info(
f"Bids: {order_book_bids}, Asks: {order_book_asks}, Delta: {bids_ask_delta}, " f"{bids}, {asks}, {delta}, Side: {side}"
f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, " f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, "
f"Immediate Bid Quantity: {order_book['bids'][0][1]}, " f"Immediate Bid Quantity: {order_book['bids'][0][1]}, "
f"Immediate Ask Quantity: {order_book['asks'][0][1]}." f"Immediate Ask Quantity: {order_book['asks'][0][1]}."
@ -514,68 +522,80 @@ class FreqtradeBot(LoggingMixin):
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
return False return False
def _check_depth_of_market_short(self, pair: str, conf: Dict) -> bool: def execute_enter(
# TODO-mg: implement, use _check_depth_of_market_buy instead if possible self,
return False pair: str,
stake_amount: float,
def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None, price: Optional[float] = None,
forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool: forcebuy: bool = False,
leverage: Optional[float] = 1.0,
is_short: bool = False,
buy_tag: Optional[str] = None
) -> bool:
""" """
Executes a limit buy for the given pair Executes a limit buy for the given pair
:param pair: pair for which we want to create a LIMIT_BUY :param pair: pair for which we want to create a LIMIT_BUY
:param stake_amount: amount of stake-currency for the pair :param stake_amount: amount of stake-currency for the pair
:param leverage: amount of leverage applied to this trade
: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'] time_in_force = self.strategy.order_time_in_force['buy'] # TODO-mg Change to enter
trade_type = 'short' if is_short else 'buy'
if price: if price:
buy_limit_requested = price enter_limit_requested = price
else: else:
# Calculate price # Calculate price
buy_limit_requested = self.exchange.get_rate(pair, refresh=True, side="buy") enter_limit_requested = self.exchange.get_rate(pair, refresh=True, side=trade_type)
if not buy_limit_requested: if not enter_limit_requested:
raise PricingError('Could not determine buy price.') raise PricingError('Could not determine buy price.')
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, buy_limit_requested, min_stake_amount = self.exchange.get_min_pair_stake_amount(
self.strategy.stoploss) pair=pair,
price=enter_limit_requested,
stoploss=self.strategy.stoploss,
leverage=leverage
)
if not self.edge: if not self.edge:
max_stake_amount = self.wallets.get_available_stake_amount() max_stake_amount = self.wallets.get_available_stake_amount()
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
default_retval=stake_amount)( default_retval=stake_amount)(
pair=pair, current_time=datetime.now(timezone.utc), pair=pair, current_time=datetime.now(timezone.utc),
current_rate=buy_limit_requested, proposed_stake=stake_amount, current_rate=enter_limit_requested, proposed_stake=stake_amount,
min_stake=min_stake_amount, max_stake=max_stake_amount) min_stake=min_stake_amount, max_stake=max_stake_amount)
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
if not stake_amount: if not stake_amount:
return False return False
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: " log_type = f"{trade_type.capitalize()} signal found"
logger.info(f"{log_type}: about create a new trade for {pair} with stake_amount: "
f"{stake_amount} ...") f"{stake_amount} ...")
amount = stake_amount / buy_limit_requested amount = stake_amount / enter_limit_requested
order_type = self.strategy.order_types['buy'] order_type = self.strategy.order_types[trade_type]
if forcebuy: if forcebuy:
# Forcebuy can define a different ordertype # Forcebuy can define a different ordertype
# TODO-mg get a forceshort
order_type = self.strategy.order_types.get('forcebuy', order_type) order_type = self.strategy.order_types.get('forcebuy', order_type)
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=buy_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 buying {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="buy",
amount=amount, rate=buy_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, 'buy')
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 = buy_limit_requested buy_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':
@ -617,7 +637,7 @@ class FreqtradeBot(LoggingMixin):
fee_open=fee, fee_open=fee,
fee_close=fee, fee_close=fee,
open_rate=buy_limit_filled_price, open_rate=buy_limit_filled_price,
open_rate_requested=buy_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,
@ -637,21 +657,17 @@ class FreqtradeBot(LoggingMixin):
# Updating wallets # Updating wallets
self.wallets.update() self.wallets.update()
self._notify_buy(trade, order_type) self._notify_open(trade, order_type)
return True return True
def execute_short(self, pair: str, stake_amount: float, price: Optional[float] = None, def _notify_open(self, trade: Trade, order_type: str) -> None:
forcesell: bool = False) -> bool:
return False # TODO-mg: implement
def _notify_buy(self, trade: Trade, order_type: str) -> None:
""" """
Sends rpc notification when a buy occurred. Sends rpc notification when a buy/short occurred.
""" """
msg = { msg = {
'trade_id': trade.id, 'trade_id': trade.id,
'type': RPCMessageType.BUY, 'type': RPCMessageType.SHORT if trade.is_short else RPCMessageType.BUY,
'buy_tag': trade.buy_tag, 'buy_tag': trade.buy_tag,
'exchange': self.exchange.name.capitalize(), 'exchange': self.exchange.name.capitalize(),
'pair': trade.pair, 'pair': trade.pair,
@ -668,15 +684,15 @@ class FreqtradeBot(LoggingMixin):
# Send the message # Send the message
self.rpc.send_msg(msg) self.rpc.send_msg(msg)
def _notify_buy_cancel(self, trade: Trade, order_type: str, reason: str) -> None: def _notify_open_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
""" """
Sends rpc notification when a buy cancel occurred. Sends rpc notification when a buy/short cancel occurred.
""" """
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy") current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy")
msg_type = RPCMessageType.SHORT_CANCEL if trade.is_short else RPCMessageType.BUY_CANCEL
msg = { msg = {
'trade_id': trade.id, 'trade_id': trade.id,
'type': RPCMessageType.BUY_CANCEL, 'type': msg_type,
'buy_tag': trade.buy_tag, 'buy_tag': trade.buy_tag,
'exchange': self.exchange.name.capitalize(), 'exchange': self.exchange.name.capitalize(),
'pair': trade.pair, 'pair': trade.pair,
@ -694,10 +710,11 @@ class FreqtradeBot(LoggingMixin):
# Send the message # Send the message
self.rpc.send_msg(msg) self.rpc.send_msg(msg)
def _notify_buy_fill(self, trade: Trade) -> None: def _notify_open_fill(self, trade: Trade) -> None:
msg_type = RPCMessageType.SHORT_FILL if trade.is_short else RPCMessageType.BUY_FILL
msg = { msg = {
'trade_id': trade.id, 'trade_id': trade.id,
'type': RPCMessageType.BUY_FILL, 'type': msg_type,
'buy_tag': trade.buy_tag, 'buy_tag': trade.buy_tag,
'exchange': self.exchange.name.capitalize(), 'exchange': self.exchange.name.capitalize(),
'pair': trade.pair, 'pair': trade.pair,
@ -716,7 +733,7 @@ class FreqtradeBot(LoggingMixin):
def exit_positions(self, trades: List[Any]) -> int: def exit_positions(self, trades: List[Any]) -> int:
""" """
Tries to execute sell orders for open trades (positions) Tries to execute sell/exit_short orders for open trades (positions)
""" """
trades_closed = 0 trades_closed = 0
for trade in trades: for trade in trades:
@ -732,7 +749,7 @@ class FreqtradeBot(LoggingMixin):
trades_closed += 1 trades_closed += 1
except DependencyException as exception: except DependencyException as exception:
logger.warning('Unable to sell trade %s: %s', trade.pair, exception) logger.warning('Unable to exit trade %s: %s', trade.pair, exception)
# Updating wallets if any trade occurred # Updating wallets if any trade occurred
if trades_closed: if trades_closed:
@ -742,33 +759,42 @@ class FreqtradeBot(LoggingMixin):
def handle_trade(self, trade: Trade) -> bool: def handle_trade(self, trade: Trade) -> bool:
""" """
Sells the current pair if the threshold is reached and updates the trade record. Sells/exits_short the current pair if the threshold is reached and updates the trade record.
:return: True if trade has been sold, False otherwise :return: True if trade has been sold/exited_short, False otherwise
""" """
if not trade.is_open: if not trade.is_open:
raise DependencyException(f'Attempt to handle closed trade: {trade}') raise DependencyException(f'Attempt to handle closed trade: {trade}')
logger.debug('Handling %s ...', trade) logger.debug('Handling %s ...', trade)
(buy, sell) = (False, False) (enter_trade, exit_trade) = (False, False)
if (self.config.get('use_sell_signal', True) or if trade.is_short:
self.config.get('ignore_roi_if_buy_signal', False)): # enter_signal = "short"
exit_signal = "exit_short"
signal_config = (self.config.get('use_exit_short_signal', True)
or self.config.get('ignore_roi_if_short_signal', False))
else:
# enter_signal = "buy"
exit_signal = "sell"
signal_config = (self.config.get('use_sell_signal', True)
or self.config.get('ignore_roi_if_buy_signal', False))
if (signal_config):
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
self.strategy.timeframe) self.strategy.timeframe)
(enter_trade, exit_trade, _) = self.strategy.get_signal(
trade.pair, self.strategy.timeframe, analyzed_df)
(buy, sell, _) = self.strategy.get_signal(
trade.pair,
self.strategy.timeframe,
analyzed_df
)
logger.debug('checking sell')
sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
return True
logger.debug('Found no sell signal for %s.', trade) # TODO-mg: This will not works for shorts,
# TODO-mg: update exchange.get_exit_rate and _check_and_execute_sell
if self._check_and_execute_sell(trade, sell_rate, enter_trade, exit_trade):
return True
# TODO-mg: end of non-working section
logger.debug(f'Found no {exit_signal} signal for %s.', trade)
return False return False
def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool: def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
@ -795,9 +821,14 @@ class FreqtradeBot(LoggingMixin):
except InvalidOrderException as e: except InvalidOrderException as e:
trade.stoploss_order_id = None trade.stoploss_order_id = None
logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.error(f'Unable to place a stoploss order on exchange. {e}')
logger.warning('Selling the trade forcefully') if trade.is_short:
self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple( logger.warning('Exiting(Buying) the trade forcefully')
sell_type=SellType.EMERGENCY_SELL)) # self.execute_exit_short(trade, trade.stop_loss, sell_reason=SellCheckTuple(
# sell_type=SellType.EMERGENCY_SELL))
else:
logger.warning('Selling the trade forcefully')
self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple(
sell_type=SellType.EMERGENCY_SELL))
except ExchangeError: except ExchangeError:
trade.stoploss_order_id = None trade.stoploss_order_id = None
@ -809,6 +840,8 @@ class FreqtradeBot(LoggingMixin):
Check if trade is fulfilled in which case the stoploss Check if trade is fulfilled in which case the stoploss
on exchange should be added immediately if stoploss on exchange on exchange should be added immediately if stoploss on exchange
is enabled. is enabled.
# TODO-mg: liquidation price will always be on exchange, even though
# TODO-mg: stoploss_on_exchange might not be enabled
""" """
logger.debug('Handling stoploss on exchange %s ...', trade) logger.debug('Handling stoploss on exchange %s ...', trade)
@ -827,13 +860,15 @@ class FreqtradeBot(LoggingMixin):
# We check if stoploss order is fulfilled # We check if stoploss order is fulfilled
if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
# TODO-mg: Update to exit/close reason (doesn't affect code functionality)
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
stoploss_order=True) stoploss_order=True)
# Lock pair for one candle to prevent immediate rebuys # Lock pair for one candle to prevent immediate rebuys
# TODO: Disble this lock for market making strategies
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
reason='Auto lock') reason='Auto lock')
self._notify_sell(trade, "stoploss") self._notify_close(trade, "stoploss")
return True return True
if trade.open_order_id or not trade.is_open: if trade.open_order_id or not trade.is_open:
@ -842,10 +877,13 @@ class FreqtradeBot(LoggingMixin):
# The trade can be closed already (sell-order fill confirmation came in this iteration) # The trade can be closed already (sell-order fill confirmation came in this iteration)
return False return False
# If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange
if not stoploss_order: if not stoploss_order:
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
stop_price = trade.open_rate * (1 + stoploss) if trade.is_short:
stop_price = trade.open_rate * (1 - stoploss)
else:
stop_price = trade.open_rate * (1 + stoploss)
if self.create_stoploss_order(trade=trade, stop_price=stop_price): if self.create_stoploss_order(trade=trade, stop_price=stop_price):
trade.stoploss_last_update = datetime.utcnow() trade.stoploss_last_update = datetime.utcnow()
@ -906,6 +944,7 @@ class FreqtradeBot(LoggingMixin):
buy: bool, sell: bool) -> bool: buy: bool, sell: bool) -> bool:
""" """
Check and execute sell Check and execute sell
# TODO-mg: Update this for shorts
""" """
should_sell = self.strategy.should_sell( should_sell = self.strategy.should_sell(
trade, sell_rate, datetime.now(timezone.utc), buy, sell, trade, sell_rate, datetime.now(timezone.utc), buy, sell,
@ -950,14 +989,15 @@ class FreqtradeBot(LoggingMixin):
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and ( if (order['side'] == trade.enter_side and (
order['status'] == 'open' or fully_cancelled) and (
fully_cancelled fully_cancelled
or self._check_timed_out('buy', order) or self._check_timed_out(trade.enter_side, order)
or strategy_safe_wrapper(self.strategy.check_buy_timeout, or strategy_safe_wrapper(self.strategy.check_buy_timeout,
default_retval=False)(pair=trade.pair, default_retval=False)(pair=trade.pair,
trade=trade, trade=trade,
order=order))): order=order))):
self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['TIMEOUT']) self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and ( elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
fully_cancelled fully_cancelled
@ -982,13 +1022,13 @@ class FreqtradeBot(LoggingMixin):
continue continue
if order['side'] == 'buy': if order['side'] == 'buy':
self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
elif order['side'] == 'sell': elif order['side'] == 'sell':
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
Trade.commit() Trade.commit()
def handle_cancel_buy(self, trade: Trade, order: Dict, reason: str) -> bool: def handle_cancel_enter(self, trade: Trade, order: Dict, reason: str) -> bool:
""" """
Buy cancel - cancel order Buy cancel - cancel order
:return: True if order was fully cancelled :return: True if order was fully cancelled
@ -1005,7 +1045,7 @@ class FreqtradeBot(LoggingMixin):
if filled_val > 0 and filled_stake < minstake: if filled_val > 0 and filled_stake < minstake:
logger.warning( logger.warning(
f"Order {trade.open_order_id} for {trade.pair} not cancelled, " f"Order {trade.open_order_id} for {trade.pair} not cancelled, "
f"as the filled amount of {filled_val} would result in an unsellable trade.") f"as the filled amount of {filled_val} would result in an uncloseable trade.")
return False return False
corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
trade.amount) trade.amount)
@ -1020,12 +1060,19 @@ class FreqtradeBot(LoggingMixin):
corder = order corder = order
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
# TODO-mg: get this side variable working, fails in tests
# side = trade.enter_side[0].upper() + trade.enter_side[1:].lower()
logger.info('Buy order %s for %s.', reason, trade) logger.info('Buy order %s for %s.', reason, trade)
# logger.info(f'{side} order %s for %s.', reason, trade)
# Using filled to determine the filled amount # Using filled to determine the filled amount
filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled') filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled')
if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC): if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC):
logger.info('Buy order fully cancelled. Removing %s from database.', trade) logger.info(
'Buy order fully cancelled. Removing %s from database.',
# f'{side} order fully cancelled. Removing %s from database.',
trade
)
# if trade is not partially completed, just delete the trade # if trade is not partially completed, just delete the trade
trade.delete() trade.delete()
was_trade_fully_canceled = True was_trade_fully_canceled = True
@ -1036,17 +1083,21 @@ class FreqtradeBot(LoggingMixin):
# cancel_order may not contain the full order dict, so we need to fallback # cancel_order may not contain the full order dict, so we need to fallback
# to the order dict aquired before cancelling. # to the order dict aquired before cancelling.
# we need to fall back to the values from order if corder does not contain these keys. # we need to fall back to the values from order if corder does not contain these keys.
# TODO-mg: Update trade.leverage if the full buy order was not filled
trade.amount = filled_amount trade.amount = filled_amount
trade.stake_amount = trade.amount * trade.open_rate trade.stake_amount = trade.amount * trade.open_rate
self.update_trade_state(trade, trade.open_order_id, corder) self.update_trade_state(trade, trade.open_order_id, corder)
trade.open_order_id = None trade.open_order_id = None
# TODO-mg change to buy or short order
logger.info('Partial buy order timeout for %s.', trade) logger.info('Partial buy order timeout for %s.', trade)
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
self.wallets.update() self.wallets.update()
self._notify_buy_cancel(trade, order_type=self.strategy.order_types['buy'], # TODO-mg change to buy or short order
reason=reason) # TODO-mg: Or should it be self.strategy.order_types['short'] or strategy.order_types['buy']
self._notify_open_cancel(trade, order_type=self.strategy.order_types['buy'],
reason=reason)
return was_trade_fully_canceled return was_trade_fully_canceled
def handle_cancel_sell(self, trade: Trade, order: Dict, reason: str) -> str: def handle_cancel_sell(self, trade: Trade, order: Dict, reason: str) -> str:
@ -1083,7 +1134,7 @@ class FreqtradeBot(LoggingMixin):
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
self.wallets.update() self.wallets.update()
self._notify_sell_cancel( self._notify_close_cancel(
trade, trade,
order_type=self.strategy.order_types['sell'], order_type=self.strategy.order_types['sell'],
reason=reason reason=reason
@ -1123,6 +1174,7 @@ class FreqtradeBot(LoggingMixin):
:param sell_reason: Reason the sell was triggered :param sell_reason: Reason the sell was triggered
:return: True if it succeeds (supported) False (not supported) :return: True if it succeeds (supported) False (not supported)
""" """
# TODO-mg: account for leverage and make this work for shorts
sell_type = 'sell' sell_type = 'sell'
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
sell_type = 'stoploss' sell_type = 'stoploss'
@ -1190,11 +1242,11 @@ class FreqtradeBot(LoggingMixin):
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
reason='Auto lock') reason='Auto lock')
self._notify_sell(trade, order_type) self._notify_close(trade, order_type)
return True return True
def _notify_sell(self, trade: Trade, order_type: str, fill: bool = False) -> None: def _notify_close(self, trade: Trade, order_type: str, fill: bool = False) -> None:
""" """
Sends rpc notification when a sell occurred. Sends rpc notification when a sell occurred.
""" """
@ -1236,7 +1288,7 @@ class FreqtradeBot(LoggingMixin):
# Send the message # Send the message
self.rpc.send_msg(msg) self.rpc.send_msg(msg)
def _notify_sell_cancel(self, trade: Trade, order_type: str, reason: str) -> None: def _notify_close_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
""" """
Sends rpc notification when a sell cancel occurred. Sends rpc notification when a sell cancel occurred.
""" """
@ -1331,13 +1383,13 @@ class FreqtradeBot(LoggingMixin):
# Updating wallets when order is closed # Updating wallets when order is closed
if not trade.is_open: if not trade.is_open:
if not stoploss_order and not trade.open_order_id: if not stoploss_order and not trade.open_order_id:
self._notify_sell(trade, '', True) self._notify_close(trade, '', True)
self.protections.stop_per_pair(trade.pair) self.protections.stop_per_pair(trade.pair)
self.protections.global_stop() self.protections.global_stop()
self.wallets.update() self.wallets.update()
elif not trade.open_order_id: elif not trade.open_order_id:
# Buy fill # Buy fill
self._notify_buy_fill(trade) self._notify_open_fill(trade)
return False return False
@ -1350,6 +1402,7 @@ class FreqtradeBot(LoggingMixin):
self.wallets.update() self.wallets.update()
if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount: if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount:
# Eat into dust if we own more than base currency # Eat into dust if we own more than base currency
# TODO-mg: won't be in "base"(quote) currency for shorts
logger.info(f"Fee amount for {trade} was in base currency - " logger.info(f"Fee amount for {trade} was in base currency - "
f"Eating Fee {fee_abs} into dust.") f"Eating Fee {fee_abs} into dust.")
elif fee_abs != 0: elif fee_abs != 0:
@ -1361,6 +1414,7 @@ class FreqtradeBot(LoggingMixin):
def get_real_amount(self, trade: Trade, order: Dict) -> float: def get_real_amount(self, trade: Trade, order: Dict) -> float:
""" """
TODO-mg: Update this function to account for shorts
Detect and update trade fee. Detect and update trade fee.
Calls trade.update_fee() upon correct detection. Calls trade.update_fee() upon correct detection.
Returns modified amount if the fee was taken from the destination currency. Returns modified amount if the fee was taken from the destination currency.
@ -1393,6 +1447,7 @@ class FreqtradeBot(LoggingMixin):
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float: def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float:
""" """
TODO-mg: Update this function to account for shorts
fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee. fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee.
""" """
trades = self.exchange.get_trades_for_order(self.exchange.get_order_id_conditional(order), trades = self.exchange.get_trades_for_order(self.exchange.get_order_id_conditional(order),

View File

@ -36,6 +36,7 @@ class RPCException(Exception):
raise RPCException('*Status:* `no active trade`') raise RPCException('*Status:* `no active trade`')
""" """
# TODO-mg: Add new configuration options introduced with leveraged/short trading
def __init__(self, message: str) -> None: def __init__(self, message: str) -> None:
super().__init__(self) super().__init__(self)
@ -545,7 +546,7 @@ class RPC:
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
if order['side'] == 'buy': if order['side'] == 'buy':
fully_canceled = self._freqtrade.handle_cancel_buy( fully_canceled = self._freqtrade.handle_cancel_enter(
trade, order, CANCEL_REASON['FORCE_SELL']) trade, order, CANCEL_REASON['FORCE_SELL'])
if order['side'] == 'sell': if order['side'] == 'sell':
@ -613,7 +614,7 @@ class RPC:
stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair) stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair)
# execute buy # execute buy
if self._freqtrade.execute_buy(pair, stakeamount, price, forcebuy=True): if self._freqtrade.execute_enter(pair, stakeamount, price, forcebuy=True):
Trade.commit() Trade.commit()
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
return trade return trade

View File

@ -185,7 +185,7 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b
limit_buy_order_open['id'] = str(i) limit_buy_order_open['id'] = str(i)
result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC') result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC')
assert pytest.approx(result) == expected[i] assert pytest.approx(result) == expected[i]
freqtrade.execute_buy('ETH/BTC', result) freqtrade.execute_enter('ETH/BTC', result)
else: else:
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
freqtrade.wallets.get_trade_stake_amount('ETH/BTC') freqtrade.wallets.get_trade_stake_amount('ETH/BTC')
@ -584,8 +584,8 @@ def test_create_trades_preopen(default_conf, ticker, fee, mocker, limit_buy_orde
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
# Create 2 existing trades # Create 2 existing trades
freqtrade.execute_buy('ETH/BTC', default_conf['stake_amount']) freqtrade.execute_enter('ETH/BTC', default_conf['stake_amount'])
freqtrade.execute_buy('NEO/BTC', default_conf['stake_amount']) freqtrade.execute_enter('NEO/BTC', default_conf['stake_amount'])
assert len(Trade.get_open_trades()) == 2 assert len(Trade.get_open_trades()) == 2
# Change order_id for new orders # Change order_id for new orders
@ -776,7 +776,7 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None:
assert ("ETH/BTC", default_conf["timeframe"]) in refresh_mock.call_args[0][0] assert ("ETH/BTC", default_conf["timeframe"]) in refresh_mock.call_args[0][0]
def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order_open) -> None: def test_execute_enter(mocker, default_conf, fee, limit_buy_order, limit_buy_order_open) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
@ -784,7 +784,9 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
stake_amount = 2 stake_amount = 2
bid = 0.11 bid = 0.11
buy_rate_mock = MagicMock(return_value=bid) buy_rate_mock = MagicMock(return_value=bid)
buy_mm = MagicMock(return_value=limit_buy_order_open) buy_mm = MagicMock(
return_value=limit_buy_order_open
)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
get_rate=buy_rate_mock, get_rate=buy_rate_mock,
@ -799,7 +801,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
) )
pair = 'ETH/BTC' pair = 'ETH/BTC'
assert not freqtrade.execute_buy(pair, stake_amount) assert not freqtrade.execute_enter(pair, stake_amount)
assert buy_rate_mock.call_count == 1 assert buy_rate_mock.call_count == 1
assert buy_mm.call_count == 0 assert buy_mm.call_count == 0
assert freqtrade.strategy.confirm_trade_entry.call_count == 1 assert freqtrade.strategy.confirm_trade_entry.call_count == 1
@ -807,7 +809,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
limit_buy_order_open['id'] = '22' limit_buy_order_open['id'] = '22'
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
assert freqtrade.execute_buy(pair, stake_amount) assert freqtrade.execute_enter(pair, stake_amount)
assert buy_rate_mock.call_count == 1 assert buy_rate_mock.call_count == 1
assert buy_mm.call_count == 1 assert buy_mm.call_count == 1
call_args = buy_mm.call_args_list[0][1] call_args = buy_mm.call_args_list[0][1]
@ -826,7 +828,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
# Test calling with price # Test calling with price
limit_buy_order_open['id'] = '33' limit_buy_order_open['id'] = '33'
fix_price = 0.06 fix_price = 0.06
assert freqtrade.execute_buy(pair, stake_amount, fix_price) assert freqtrade.execute_enter(pair, stake_amount, fix_price)
# Make sure get_rate wasn't called again # Make sure get_rate wasn't called again
assert buy_rate_mock.call_count == 0 assert buy_rate_mock.call_count == 0
@ -844,7 +846,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
mocker.patch('freqtrade.exchange.Exchange.create_order', mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=limit_buy_order)) MagicMock(return_value=limit_buy_order))
assert freqtrade.execute_buy(pair, stake_amount) assert freqtrade.execute_enter(pair, stake_amount)
trade = Trade.query.all()[2] trade = Trade.query.all()[2]
assert trade assert trade
assert trade.open_order_id is None assert trade.open_order_id is None
@ -861,7 +863,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
limit_buy_order['id'] = '555' limit_buy_order['id'] = '555'
mocker.patch('freqtrade.exchange.Exchange.create_order', mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=limit_buy_order)) MagicMock(return_value=limit_buy_order))
assert freqtrade.execute_buy(pair, stake_amount) assert freqtrade.execute_enter(pair, stake_amount)
trade = Trade.query.all()[3] trade = Trade.query.all()[3]
assert trade assert trade
assert trade.open_order_id == '555' assert trade.open_order_id == '555'
@ -873,7 +875,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
limit_buy_order['id'] = '556' limit_buy_order['id'] = '556'
freqtrade.strategy.custom_stake_amount = lambda **kwargs: 150.0 freqtrade.strategy.custom_stake_amount = lambda **kwargs: 150.0
assert freqtrade.execute_buy(pair, stake_amount) assert freqtrade.execute_enter(pair, stake_amount)
trade = Trade.query.all()[4] trade = Trade.query.all()[4]
assert trade assert trade
assert trade.stake_amount == 150 assert trade.stake_amount == 150
@ -881,7 +883,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
# Exception case # Exception case
limit_buy_order['id'] = '557' limit_buy_order['id'] = '557'
freqtrade.strategy.custom_stake_amount = lambda **kwargs: 20 / 0 freqtrade.strategy.custom_stake_amount = lambda **kwargs: 20 / 0
assert freqtrade.execute_buy(pair, stake_amount) assert freqtrade.execute_enter(pair, stake_amount)
trade = Trade.query.all()[5] trade = Trade.query.all()[5]
assert trade assert trade
assert trade.stake_amount == 2.0 assert trade.stake_amount == 2.0
@ -896,16 +898,16 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
limit_buy_order['id'] = '66' limit_buy_order['id'] = '66'
mocker.patch('freqtrade.exchange.Exchange.create_order', mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=limit_buy_order)) MagicMock(return_value=limit_buy_order))
assert not freqtrade.execute_buy(pair, stake_amount) assert not freqtrade.execute_enter(pair, stake_amount)
# Fail to get price... # Fail to get price...
mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(return_value=0.0)) mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(return_value=0.0))
with pytest.raises(PricingError, match="Could not determine buy price."): with pytest.raises(PricingError, match="Could not determine buy price."):
freqtrade.execute_buy(pair, stake_amount) freqtrade.execute_enter(pair, stake_amount)
def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None: def test_execute_enter_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
@ -923,18 +925,18 @@ def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -
pair = 'ETH/BTC' pair = 'ETH/BTC'
freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=ValueError) freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=ValueError)
assert freqtrade.execute_buy(pair, stake_amount) assert freqtrade.execute_enter(pair, stake_amount)
limit_buy_order['id'] = '222' limit_buy_order['id'] = '222'
freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=Exception) freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=Exception)
assert freqtrade.execute_buy(pair, stake_amount) assert freqtrade.execute_enter(pair, stake_amount)
limit_buy_order['id'] = '2223' limit_buy_order['id'] = '2223'
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
assert freqtrade.execute_buy(pair, stake_amount) assert freqtrade.execute_enter(pair, stake_amount)
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False)
assert not freqtrade.execute_buy(pair, stake_amount) assert not freqtrade.execute_enter(pair, stake_amount)
def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None: def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None:
@ -1686,7 +1688,7 @@ def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog)
) )
n = freqtrade.exit_positions(trades) n = freqtrade.exit_positions(trades)
assert n == 0 assert n == 0
assert log_has('Unable to sell trade ETH/BTC: ', caplog) assert log_has('Unable to exit trade ETH/BTC: ', caplog)
def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> None: def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> None:
@ -2419,7 +2421,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot', 'freqtrade.freqtradebot.FreqtradeBot',
handle_cancel_buy=MagicMock(), handle_cancel_enter=MagicMock(),
handle_cancel_sell=MagicMock(), handle_cancel_sell=MagicMock(),
) )
mocker.patch.multiple( mocker.patch.multiple(
@ -2441,7 +2443,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke
caplog) caplog)
def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> None: def test_handle_cancel_enter(mocker, caplog, default_conf, limit_buy_order) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
cancel_buy_order = deepcopy(limit_buy_order) cancel_buy_order = deepcopy(limit_buy_order)
@ -2452,54 +2454,56 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non
mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock) mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
freqtrade._notify_buy_cancel = MagicMock() freqtrade._notify_open_cancel = MagicMock()
trade = MagicMock() trade = MagicMock()
trade.pair = 'LTC/USDT' trade.pair = 'LTC/USDT'
trade.open_rate = 200 trade.open_rate = 200
trade.is_short = False
trade.enter_side = "buy"
limit_buy_order['filled'] = 0.0 limit_buy_order['filled'] = 0.0
limit_buy_order['status'] = 'open' limit_buy_order['status'] = 'open'
reason = CANCEL_REASON['TIMEOUT'] reason = CANCEL_REASON['TIMEOUT']
assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) assert freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
cancel_order_mock.reset_mock() cancel_order_mock.reset_mock()
caplog.clear() caplog.clear()
limit_buy_order['filled'] = 0.01 limit_buy_order['filled'] = 0.01
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
assert cancel_order_mock.call_count == 0 assert cancel_order_mock.call_count == 0
assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unsellable.*", caplog) assert log_has_re("Order .* for .* not cancelled, as the filled amount.* uncloseable.*", caplog)
caplog.clear() caplog.clear()
cancel_order_mock.reset_mock() cancel_order_mock.reset_mock()
limit_buy_order['filled'] = 2 limit_buy_order['filled'] = 2
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
# Order remained open for some reason (cancel failed) # Order remained open for some reason (cancel failed)
cancel_buy_order['status'] = 'open' cancel_buy_order['status'] = 'open'
cancel_order_mock = MagicMock(return_value=cancel_buy_order) cancel_order_mock = MagicMock(return_value=cancel_buy_order)
mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock) mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
assert log_has_re(r"Order .* for .* not cancelled.", caplog) assert log_has_re(r"Order .* for .* not cancelled.", caplog)
@pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'], @pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'],
indirect=['limit_buy_order_canceled_empty']) indirect=['limit_buy_order_canceled_empty'])
def test_handle_cancel_buy_exchanges(mocker, caplog, default_conf, def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf,
limit_buy_order_canceled_empty) -> None: limit_buy_order_canceled_empty) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
cancel_order_mock = mocker.patch( cancel_order_mock = mocker.patch(
'freqtrade.exchange.Exchange.cancel_order_with_result', 'freqtrade.exchange.Exchange.cancel_order_with_result',
return_value=limit_buy_order_canceled_empty) return_value=limit_buy_order_canceled_empty)
nofiy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_buy_cancel') nofiy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_open_cancel')
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
reason = CANCEL_REASON['TIMEOUT'] reason = CANCEL_REASON['TIMEOUT']
trade = MagicMock() trade = MagicMock()
trade.pair = 'LTC/ETH' trade.pair = 'LTC/ETH'
assert freqtrade.handle_cancel_buy(trade, limit_buy_order_canceled_empty, reason) assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason)
assert cancel_order_mock.call_count == 0 assert cancel_order_mock.call_count == 0
assert log_has_re(r'Buy order fully cancelled. Removing .* from database\.', caplog) assert log_has_re(r'Buy order fully cancelled. Removing .* from database\.', caplog)
assert nofiy_mock.call_count == 1 assert nofiy_mock.call_count == 1
@ -2511,8 +2515,8 @@ def test_handle_cancel_buy_exchanges(mocker, caplog, default_conf,
'String Return value', 'String Return value',
123 123
]) ])
def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order, def test_handle_cancel_enter_corder_empty(mocker, default_conf, limit_buy_order,
cancelorder) -> None: cancelorder) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
cancel_order_mock = MagicMock(return_value=cancelorder) cancel_order_mock = MagicMock(return_value=cancelorder)
@ -2522,7 +2526,7 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order,
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
freqtrade._notify_buy_cancel = MagicMock() freqtrade._notify_open_cancel = MagicMock()
trade = MagicMock() trade = MagicMock()
trade.pair = 'LTC/USDT' trade.pair = 'LTC/USDT'
@ -2530,12 +2534,12 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order,
limit_buy_order['filled'] = 0.0 limit_buy_order['filled'] = 0.0
limit_buy_order['status'] = 'open' limit_buy_order['status'] = 'open'
reason = CANCEL_REASON['TIMEOUT'] reason = CANCEL_REASON['TIMEOUT']
assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) assert freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
cancel_order_mock.reset_mock() cancel_order_mock.reset_mock()
limit_buy_order['filled'] = 1.0 limit_buy_order['filled'] = 1.0
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
@ -4099,7 +4103,7 @@ def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None:
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
conf = default_conf['bid_strategy']['check_depth_of_market'] conf = default_conf['bid_strategy']['check_depth_of_market']
assert freqtrade._check_depth_of_market_buy('ETH/BTC', conf) is False assert freqtrade._check_depth_of_market('ETH/BTC', conf, side="buy") is False
def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_order, fee, def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_order, fee,
@ -4216,7 +4220,7 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi
mocker.patch('freqtrade.exchange.Exchange.fetch_order', mocker.patch('freqtrade.exchange.Exchange.fetch_order',
side_effect=[ side_effect=[
ExchangeError(), limit_sell_order, limit_buy_order, limit_sell_order]) ExchangeError(), limit_sell_order, limit_buy_order, limit_sell_order])
buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy') buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_enter')
sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell') sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell')
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)

View File

@ -70,7 +70,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot', 'freqtrade.freqtradebot.FreqtradeBot',
create_stoploss_order=MagicMock(return_value=True), create_stoploss_order=MagicMock(return_value=True),
_notify_sell=MagicMock(), _notify_close=MagicMock(),
) )
mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock)
wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock()) wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock())
@ -154,7 +154,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot', 'freqtrade.freqtradebot.FreqtradeBot',
create_stoploss_order=MagicMock(return_value=True), create_stoploss_order=MagicMock(return_value=True),
_notify_sell=MagicMock(), _notify_close=MagicMock(),
) )
should_sell_mock = MagicMock(side_effect=[ should_sell_mock = MagicMock(side_effect=[
SellCheckTuple(sell_type=SellType.NONE), SellCheckTuple(sell_type=SellType.NONE),

View File

@ -157,13 +157,13 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r
assert result == result1 assert result == result1
# create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)'
freqtrade.execute_buy('ETH/USDT', result) freqtrade.execute_enter('ETH/USDT', result)
result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT') result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT')
assert result == result1 assert result == result1
# create 2 trades, order amount should be None # create 2 trades, order amount should be None
freqtrade.execute_buy('LTC/BTC', result) freqtrade.execute_enter('LTC/BTC', result)
result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT') result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT')
assert result == 0 assert result == 0