Short side options in freqtradebot
This commit is contained in:
parent
9f16464b12
commit
cb155764eb
@ -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,7 +851,10 @@ 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
|
||||||
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()
|
||||||
@ -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
|
||||||
trade=trade,
|
strategy_safe_wrapper(
|
||||||
order=order))):
|
self.strategy.check_buy_timeout,
|
||||||
|
default_retval=False
|
||||||
|
)(
|
||||||
|
pair=trade.pair,
|
||||||
|
trade=trade,
|
||||||
|
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
|
||||||
trade=trade,
|
strategy_safe_wrapper(
|
||||||
order=order))):
|
self.strategy.check_sell_timeout,
|
||||||
|
default_retval=False
|
||||||
|
)(
|
||||||
|
pair=trade.pair,
|
||||||
|
trade=trade,
|
||||||
|
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
1516
tests/freqtradebot.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user