Short side options in freqtradebot

This commit is contained in:
Sam Germain 2021-09-10 11:34:57 -06:00
parent 9f16464b12
commit cb155764eb
3 changed files with 1669 additions and 80 deletions

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 RPCMessageType, SellType, SignalDirection, 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
@ -272,21 +272,26 @@ class FreqtradeBot(LoggingMixin):
trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees() trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees()
for trade in trades: for trade in trades:
if not trade.is_open and not trade.fee_updated(trade.exit_side):
if not trade.is_open and not trade.fee_updated('sell'):
# Get sell fee # Get sell fee
order = trade.select_order('sell', False) order = trade.select_order(trade.exit_side, False)
if order: if order:
logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.") logger.info(
f"Updating {trade.exit_side}-fee on trade {trade}"
f"for order {order.order_id}."
)
self.update_trade_state(trade, order.order_id, self.update_trade_state(trade, order.order_id,
stoploss_order=order.ft_order_side == 'stoploss') stoploss_order=order.ft_order_side == 'stoploss')
trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
for trade in trades: for trade in trades:
if trade.is_open and not trade.fee_updated('buy'): if trade.is_open and not trade.fee_updated(trade.enter_side):
order = trade.select_order('buy', False) order = trade.select_order(trade.enter_side, False)
if order: if order:
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") logger.info(
f"Updating {trade.enter_side}-fee on trade {trade}"
f"for order {order.order_id}."
)
self.update_trade_state(trade, order.order_id) self.update_trade_state(trade, order.order_id)
def handle_insufficient_funds(self, trade: Trade): def handle_insufficient_funds(self, trade: Trade):
@ -294,8 +299,8 @@ class FreqtradeBot(LoggingMixin):
Determine if we ever opened a exiting order for this trade. Determine if we ever opened a exiting order for this trade.
If not, try update entering fees - otherwise "refind" the open order we obviously lost. If not, try update entering fees - otherwise "refind" the open order we obviously lost.
""" """
sell_order = trade.select_order('sell', None) exit_order = trade.select_order(trade.exit_side, None)
if sell_order: if exit_order:
self.refind_lost_order(trade) self.refind_lost_order(trade)
else: else:
self.reupdate_enter_order_fees(trade) self.reupdate_enter_order_fees(trade)
@ -305,10 +310,11 @@ class FreqtradeBot(LoggingMixin):
Get buy order from database, and try to reupdate. Get buy order from database, and try to reupdate.
Handles trades where the initial fee-update did not work. Handles trades where the initial fee-update did not work.
""" """
logger.info(f"Trying to reupdate buy fees for {trade}") logger.info(f"Trying to reupdate {trade.enter_side} fees for {trade}")
order = trade.select_order('buy', False) order = trade.select_order(trade.enter_side, False)
if order: if order:
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") logger.info(
f"Updating {trade.enter_side}-fee on trade {trade} for order {order.order_id}.")
self.update_trade_state(trade, order.order_id) self.update_trade_state(trade, order.order_id)
def refind_lost_order(self, trade): def refind_lost_order(self, trade):
@ -324,7 +330,7 @@ class FreqtradeBot(LoggingMixin):
if not order.ft_is_open: if not order.ft_is_open:
logger.debug(f"Order {order} is no longer open.") logger.debug(f"Order {order} is no longer open.")
continue continue
if order.ft_order_side == 'buy': if order.ft_order_side == trade.enter_side:
# Skip buy side - this is handled by reupdate_enter_order_fees # Skip buy side - this is handled by reupdate_enter_order_fees
continue continue
try: try:
@ -334,7 +340,7 @@ class FreqtradeBot(LoggingMixin):
if fo and fo['status'] == 'open': if fo and fo['status'] == 'open':
# Assume this as the open stoploss order # Assume this as the open stoploss order
trade.stoploss_order_id = order.order_id trade.stoploss_order_id = order.order_id
elif order.ft_order_side == 'sell': elif order.ft_order_side == trade.exit_side:
if fo and fo['status'] == 'open': if fo and fo['status'] == 'open':
# Assume this as the open order # Assume this as the open order
trade.open_order_id = order.order_id trade.open_order_id = order.order_id
@ -433,8 +439,11 @@ class FreqtradeBot(LoggingMixin):
if ((bid_check_dom.get('enabled', False)) and if ((bid_check_dom.get('enabled', False)) and
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)): (bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
# TODO-lev: Does the below need to be adjusted for shorts? # TODO-lev: Does the below need to be adjusted for shorts?
if self._check_depth_of_market_buy(pair, bid_check_dom): if self._check_depth_of_market(
# TODO-lev: pass in "enter" as side. pair,
bid_check_dom,
side=side
):
return self.execute_entry(pair, stake_amount, enter_tag=enter_tag) return self.execute_entry(pair, stake_amount, enter_tag=enter_tag)
else: else:
@ -444,7 +453,12 @@ class FreqtradeBot(LoggingMixin):
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: SignalDirection
) -> bool:
""" """
Checks depth of market before executing a buy Checks depth of market before executing a buy
""" """
@ -454,9 +468,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 == SignalDirection.LONG else order_book_asks
exit_side = order_book_asks if side == SignalDirection.LONG 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}, Direction: {side.value}"
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]}."
@ -468,21 +490,32 @@ 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 execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, def execute_entry(
forcebuy: bool = False, enter_tag: Optional[str] = None) -> bool: self,
pair: str,
stake_amount: float,
price: Optional[float] = None,
forcebuy: bool = False,
leverage: float = 1.0,
is_short: bool = False,
enter_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']
[side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long']
if price: if price:
enter_limit_requested = price enter_limit_requested = price
else: else:
# Calculate price # Calculate price
proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy") proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side=side)
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=proposed_enter_rate)( default_retval=proposed_enter_rate)(
pair=pair, current_time=datetime.now(timezone.utc), pair=pair, current_time=datetime.now(timezone.utc),
@ -491,10 +524,14 @@ class FreqtradeBot(LoggingMixin):
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate) enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
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(pair, enter_limit_requested, min_stake_amount = self.exchange.get_min_pair_stake_amount(
self.strategy.stoploss) pair,
enter_limit_requested,
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()
@ -508,10 +545,11 @@ class FreqtradeBot(LoggingMixin):
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"{name} 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 / enter_limit_requested amount = (stake_amount / enter_limit_requested) * leverage
order_type = self.strategy.order_types['buy'] order_type = self.strategy.order_types['buy']
if forcebuy: if forcebuy:
# Forcebuy can define a different ordertype # Forcebuy can define a different ordertype
@ -522,13 +560,13 @@ class FreqtradeBot(LoggingMixin):
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)
@ -541,17 +579,17 @@ class FreqtradeBot(LoggingMixin):
# 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']
@ -582,7 +620,9 @@ class FreqtradeBot(LoggingMixin):
strategy=self.strategy.get_strategy_name(), strategy=self.strategy.get_strategy_name(),
# TODO-lev: compatibility layer for buy_tag (!) # TODO-lev: compatibility layer for buy_tag (!)
buy_tag=enter_tag, buy_tag=enter_tag,
timeframe=timeframe_to_minutes(self.config['timeframe']) timeframe=timeframe_to_minutes(self.config['timeframe']),
leverage=leverage,
is_short=is_short,
) )
trade.orders.append(order_obj) trade.orders.append(order_obj)
@ -606,7 +646,7 @@ class FreqtradeBot(LoggingMixin):
""" """
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,
@ -627,11 +667,11 @@ class FreqtradeBot(LoggingMixin):
""" """
Sends rpc notification when a buy/short 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=trade.enter_side)
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,
@ -650,9 +690,10 @@ class FreqtradeBot(LoggingMixin):
self.rpc.send_msg(msg) self.rpc.send_msg(msg)
def _notify_enter_fill(self, trade: Trade) -> None: def _notify_enter_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,
@ -706,6 +747,8 @@ class FreqtradeBot(LoggingMixin):
logger.debug('Handling %s ...', trade) logger.debug('Handling %s ...', trade)
(enter, exit_) = (False, False) (enter, exit_) = (False, False)
exit_signal_type = "exit_short" if trade.is_short else "exit_long"
# TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal # TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal
if (self.config.get('use_sell_signal', True) or if (self.config.get('use_sell_signal', True) or
self.config.get('ignore_roi_if_buy_signal', False)): self.config.get('ignore_roi_if_buy_signal', False)):
@ -715,15 +758,16 @@ class FreqtradeBot(LoggingMixin):
(enter, exit_) = self.strategy.get_exit_signal( (enter, exit_) = self.strategy.get_exit_signal(
trade.pair, trade.pair,
self.strategy.timeframe, self.strategy.timeframe,
analyzed_df, is_short=trade.is_short analyzed_df,
is_short=trade.is_short
) )
logger.debug('checking sell') logger.debug('checking exit')
exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side=trade.exit_side)
if self._check_and_execute_exit(trade, exit_rate, enter, exit_): if self._check_and_execute_exit(trade, exit_rate, enter, exit_):
return True return True
logger.debug('Found no sell signal for %s.', trade) logger.debug(f'Found no {exit_signal_type} 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:
@ -807,6 +851,9 @@ class FreqtradeBot(LoggingMixin):
# If enter 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
if trade.is_short:
stop_price = trade.open_rate * (1 - stoploss)
else:
stop_price = trade.open_rate * (1 + stoploss) 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):
@ -844,7 +891,7 @@ class FreqtradeBot(LoggingMixin):
:param order: Current on exchange stoploss order :param order: Current on exchange stoploss order
:return: None :return: None
""" """
if self.exchange.stoploss_adjust(trade.stop_loss, order, side): if self.exchange.stoploss_adjust(trade.stop_loss, order, side=trade.exit_side):
# we check if the update is necessary # we check if the update is necessary
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
@ -912,22 +959,38 @@ 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 (
fully_cancelled order['side'] == trade.enter_side and
or self._check_timed_out('buy', order) (order['status'] == 'open' or fully_cancelled) and
or strategy_safe_wrapper(self.strategy.check_buy_timeout, (fully_cancelled or
default_retval=False)(pair=trade.pair, self._check_timed_out(trade.enter_side, order) or
strategy_safe_wrapper(
self.strategy.check_buy_timeout,
default_retval=False
)(
pair=trade.pair,
trade=trade, trade=trade,
order=order))): order=order
)
)
):
self.handle_cancel_enter(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 (
fully_cancelled order['side'] == trade.exit_side and
or self._check_timed_out('sell', order) (order['status'] == 'open' or fully_cancelled) and
or strategy_safe_wrapper(self.strategy.check_sell_timeout, (fully_cancelled or
default_retval=False)(pair=trade.pair, self._check_timed_out(trade.exit_side, order) or
strategy_safe_wrapper(
self.strategy.check_sell_timeout,
default_retval=False
)(
pair=trade.pair,
trade=trade, trade=trade,
order=order))): order=order
)
)
):
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
def cancel_all_open_orders(self) -> None: def cancel_all_open_orders(self) -> None:
@ -943,10 +1006,10 @@ class FreqtradeBot(LoggingMixin):
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
continue continue
if order['side'] == 'buy': if order['side'] == trade.enter_side:
self.handle_cancel_enter(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'] == trade.exit_side:
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
Trade.commit() Trade.commit()
@ -968,7 +1031,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 unexitable 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)
@ -983,12 +1046,16 @@ class FreqtradeBot(LoggingMixin):
corder = order corder = order
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
logger.info('Buy order %s for %s.', reason, trade) side = trade.enter_side.capitalize()
logger.info('%s order %s for %s.', side, 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(
'%s order fully cancelled. Removing %s from database.',
side, 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
@ -1006,11 +1073,11 @@ class FreqtradeBot(LoggingMixin):
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
logger.info('Partial buy order timeout for %s.', trade) logger.info('Partial %s order timeout for %s.', trade.enter_side, trade)
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
self.wallets.update() self.wallets.update()
self._notify_enter_cancel(trade, order_type=self.strategy.order_types['buy'], self._notify_enter_cancel(trade, order_type=self.strategy.order_types[trade.enter_side],
reason=reason) reason=reason)
return was_trade_fully_canceled return was_trade_fully_canceled
@ -1028,12 +1095,13 @@ class FreqtradeBot(LoggingMixin):
trade.amount) trade.amount)
trade.update_order(co) trade.update_order(co)
except InvalidOrderException: except InvalidOrderException:
logger.exception(f"Could not cancel sell order {trade.open_order_id}") logger.exception(
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
return 'error cancelling order' return 'error cancelling order'
logger.info('Sell order %s for %s.', reason, trade) logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
else: else:
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
logger.info('Sell order %s for %s.', reason, trade) logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
trade.update_order(order) trade.update_order(order)
trade.close_rate = None trade.close_rate = None
@ -1050,7 +1118,7 @@ class FreqtradeBot(LoggingMixin):
self.wallets.update() self.wallets.update()
self._notify_exit_cancel( self._notify_exit_cancel(
trade, trade,
order_type=self.strategy.order_types['sell'], order_type=self.strategy.order_types[trade.exit_side],
reason=reason reason=reason
) )
return reason return reason
@ -1189,7 +1257,7 @@ class FreqtradeBot(LoggingMixin):
profit_trade = trade.calc_profit(rate=profit_rate) profit_trade = trade.calc_profit(rate=profit_rate)
# Use cached rates here - it was updated seconds ago. # Use cached rates here - it was updated seconds ago.
current_rate = self.exchange.get_rate( current_rate = self.exchange.get_rate(
trade.pair, refresh=False, side="sell") if not fill else None trade.pair, refresh=False, side=trade.exit_side) if not fill else None
profit_ratio = trade.calc_profit_ratio(profit_rate) profit_ratio = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_ratio > 0 else "loss" gain = "profit" if profit_ratio > 0 else "loss"
@ -1234,7 +1302,7 @@ class FreqtradeBot(LoggingMixin):
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit_trade = trade.calc_profit(rate=profit_rate) profit_trade = trade.calc_profit(rate=profit_rate)
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="sell") current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.exit_side)
profit_ratio = trade.calc_profit_ratio(profit_rate) profit_ratio = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_ratio > 0 else "loss" gain = "profit" if profit_ratio > 0 else "loss"

1516
tests/freqtradebot.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@ import arrow
import pytest import pytest
from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT
from freqtrade.enums import RPCMessageType, RunMode, SellType, State from freqtrade.enums import RPCMessageType, RunMode, SellType, SignalDirection, State
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, OperationalException, PricingError, InvalidOrderException, OperationalException, PricingError,
TemporaryError) TemporaryError)
@ -631,7 +631,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, limit_buy
assert trade.amount == 91.07468123 assert trade.amount == 91.07468123
assert log_has( assert log_has(
'Buy signal found: about create a new trade for ETH/BTC with stake_amount: 0.001 ...', 'Long signal found: about create a new trade for ETH/BTC with stake_amount: 0.001 ...',
caplog caplog
) )
@ -2508,6 +2508,8 @@ def test_handle_cancel_enter(mocker, caplog, default_conf, limit_buy_order) -> N
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']
@ -2519,7 +2521,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf, limit_buy_order) -> N
limit_buy_order['filled'] = 0.01 limit_buy_order['filled'] = 0.01
assert not freqtrade.handle_cancel_enter(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.* unexitable.*", caplog)
caplog.clear() caplog.clear()
cancel_order_mock.reset_mock() cancel_order_mock.reset_mock()
@ -2550,6 +2552,7 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf,
reason = CANCEL_REASON['TIMEOUT'] reason = CANCEL_REASON['TIMEOUT']
trade = MagicMock() trade = MagicMock()
trade.pair = 'LTC/ETH' trade.pair = 'LTC/ETH'
trade.enter_side = "buy"
assert freqtrade.handle_cancel_enter(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)
@ -2577,7 +2580,9 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf, limit_buy_order,
trade = MagicMock() trade = MagicMock()
trade.pair = 'LTC/USDT' trade.pair = 'LTC/USDT'
trade.enter_side = "buy"
trade.open_rate = 200 trade.open_rate = 200
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']
@ -3374,7 +3379,7 @@ def test__safe_exit_amount_error(default_conf, fee, caplog, mocker):
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
with pytest.raises(DependencyException, match=r"Not enough amount to exit."): with pytest.raises(DependencyException, match=r"Not enough amount to exit trade."):
assert freqtrade._safe_exit_amount(trade.pair, trade.amount) assert freqtrade._safe_exit_amount(trade.pair, trade.amount)
@ -4210,7 +4215,7 @@ def test_order_book_bid_strategy_exception(mocker, default_conf, caplog) -> None
assert log_has_re(r'Buy Price at location 1 from orderbook could not be determined.', caplog) assert log_has_re(r'Buy Price at location 1 from orderbook could not be determined.', caplog)
def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None: def test_check_depth_of_market(default_conf, mocker, order_book_l2) -> None:
""" """
test check depth of market test check depth of market
""" """
@ -4227,7 +4232,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=SignalDirection.LONG) 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,