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'
WARNING = 'warning'
STARTUP = 'startup'
BUY = 'buy'
BUY_FILL = 'buy_fill'
BUY_CANCEL = 'buy_cancel'
SELL = 'sell'
SELL_FILL = 'sell_fill'
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):
return self.value

View File

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

View File

@ -1,6 +1,6 @@
""" Binance exchange subclass """
import logging
from typing import Dict
from typing import Dict, Optional
import ccxt
@ -92,3 +92,104 @@ class Binance(Exchange):
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as 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 """
import logging
from typing import Dict
from typing import Dict, Optional
from freqtrade.exchange import Exchange
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__)
@ -23,3 +24,23 @@ class Bittrex(Exchange):
},
"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'),
'password': exchange_config.get('password'),
'uid': exchange_config.get('uid', ''),
'options': exchange_config.get('options', {})
}
if ccxt_kwargs:
logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
@ -526,8 +527,9 @@ class Exchange:
else:
return 1 / pow(10, precision)
def get_min_pair_stake_amount(self, pair: str, price: float,
stoploss: float) -> Optional[float]:
def get_min_pair_stake_amount(self, pair: str, price: float, stoploss: float,
leverage: Optional[float] = 1.0) -> Optional[float]:
# TODO-mg: Using leverage makes the min stake amount lower (on binance at least)
try:
market = self.markets[pair]
except KeyError:
@ -561,7 +563,20 @@ class Exchange:
# The value returned should satisfy both limits: for amount (base currency) and
# for cost (quote, stake currency), so max() is used here.
# 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
@ -688,6 +703,15 @@ class Exchange:
raise InvalidOrderException(
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
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,
amount, rate_for_order, params)
self._log_exchange_response('create_order', order)
return order
except ccxt.InsufficientFunds as e:
@ -731,6 +756,26 @@ class Exchange:
except ccxt.BaseError as 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:
"""
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,
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:
return exchange_name in ccxt_exchanges(ccxt_module)

View File

@ -1,6 +1,6 @@
""" Kraken exchange subclass """
import logging
from typing import Any, Dict
from typing import Any, Dict, Optional
import ccxt
@ -127,3 +127,23 @@ class Kraken(Exchange):
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as 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:
"""
# 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.
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.")
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
(buy, sell, buy_tag) = self.strategy.get_signal(
pair,
@ -448,49 +452,45 @@ class FreqtradeBot(LoggingMixin):
)
(short, exit_short) = (False, False)
# = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df)
# #TODO: Update strategy class to get these values
open_buy_signal = buy and not sell
open_short_signal = short and not exit_short
# TODO-mg: Update strategy class to get these values
# 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):
stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
if not stake_amount:
logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.")
return False
# leverage = 1.0 # TODO: get leverage from somewhere
logger.info(
f"{'Buy' if open_buy_signal else 'Short'} signal found: "
f"about create a new trade for {pair} with stake_amount: "
f"{stake_amount} ..."
)
# TODO: All later code in this function
if open_buy_signal:
bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {})
strat = 'bid_strategy'
side = 'buy'
# execute_trade = self.execute_enter
lev = long_lev
else:
strat = 'short_strategy'
side = 'sell'
# execute_trade = self.execute_short
lev = short_lev
bid_check_dom = self.config.get(strat, {}).get('check_depth_of_market', {})
if ((bid_check_dom.get('enabled', False)) and
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
if self._check_depth_of_market_buy(pair, bid_check_dom):
return self.execute_buy(pair, stake_amount, buy_tag=buy_tag)
else:
return False
else:
# TODO-mg: Will check_depth_of_market_buy work for shorts?
short_check_dom = self.config.get(
'short_strategy', {}).get('check_depth_of_market', {})
if ((short_check_dom.get('enabled', False)) and
(short_check_dom.get('shorts_to_ask_delta', 0) > 0)):
if self._check_depth_of_market_short(pair, short_check_dom):
return self.execute_short(pair, stake_amount)
if self._check_depth_of_market(pair, bid_check_dom, side):
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:
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
"""
@ -500,9 +500,17 @@ class FreqtradeBot(LoggingMixin):
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_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(
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"Immediate Bid Quantity: {order_book['bids'][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.")
return False
def _check_depth_of_market_short(self, pair: str, conf: Dict) -> bool:
# TODO-mg: implement, use _check_depth_of_market_buy instead if possible
return False
def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None,
forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool:
def execute_enter(
self,
pair: str,
stake_amount: float,
price: Optional[float] = None,
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
:param pair: pair for which we want to create a LIMIT_BUY
: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.
"""
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:
buy_limit_requested = price
enter_limit_requested = price
else:
# 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.')
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, buy_limit_requested,
self.strategy.stoploss)
min_stake_amount = self.exchange.get_min_pair_stake_amount(
pair=pair,
price=enter_limit_requested,
stoploss=self.strategy.stoploss,
leverage=leverage
)
if not self.edge:
max_stake_amount = self.wallets.get_available_stake_amount()
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
default_retval=stake_amount)(
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)
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
if not stake_amount:
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} ...")
amount = stake_amount / buy_limit_requested
order_type = self.strategy.order_types['buy']
amount = stake_amount / enter_limit_requested
order_type = self.strategy.order_types[trade_type]
if forcebuy:
# Forcebuy can define a different ordertype
# TODO-mg get a forceshort
order_type = self.strategy.order_types.get('forcebuy', order_type)
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)):
logger.info(f"User requested abortion of buying {pair}")
return False
amount = self.exchange.amount_to_precision(pair, amount)
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)
order_obj = Order.parse_from_ccxt_object(order, pair, 'buy')
order_id = order['id']
order_status = order.get('status', None)
# 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
if order_status == 'expired' or order_status == 'rejected':
@ -617,7 +637,7 @@ class FreqtradeBot(LoggingMixin):
fee_open=fee,
fee_close=fee,
open_rate=buy_limit_filled_price,
open_rate_requested=buy_limit_requested,
open_rate_requested=enter_limit_requested,
open_date=datetime.utcnow(),
exchange=self.exchange.id,
open_order_id=order_id,
@ -637,21 +657,17 @@ class FreqtradeBot(LoggingMixin):
# Updating wallets
self.wallets.update()
self._notify_buy(trade, order_type)
self._notify_open(trade, order_type)
return True
def execute_short(self, pair: str, stake_amount: float, price: Optional[float] = None,
forcesell: bool = False) -> bool:
return False # TODO-mg: implement
def _notify_buy(self, trade: Trade, order_type: str) -> None:
def _notify_open(self, trade: Trade, order_type: str) -> None:
"""
Sends rpc notification when a buy occurred.
Sends rpc notification when a buy/short occurred.
"""
msg = {
'trade_id': trade.id,
'type': RPCMessageType.BUY,
'type': RPCMessageType.SHORT if trade.is_short else RPCMessageType.BUY,
'buy_tag': trade.buy_tag,
'exchange': self.exchange.name.capitalize(),
'pair': trade.pair,
@ -668,15 +684,15 @@ class FreqtradeBot(LoggingMixin):
# Send the message
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")
msg_type = RPCMessageType.SHORT_CANCEL if trade.is_short else RPCMessageType.BUY_CANCEL
msg = {
'trade_id': trade.id,
'type': RPCMessageType.BUY_CANCEL,
'type': msg_type,
'buy_tag': trade.buy_tag,
'exchange': self.exchange.name.capitalize(),
'pair': trade.pair,
@ -694,10 +710,11 @@ class FreqtradeBot(LoggingMixin):
# Send the message
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 = {
'trade_id': trade.id,
'type': RPCMessageType.BUY_FILL,
'type': msg_type,
'buy_tag': trade.buy_tag,
'exchange': self.exchange.name.capitalize(),
'pair': trade.pair,
@ -716,7 +733,7 @@ class FreqtradeBot(LoggingMixin):
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
for trade in trades:
@ -732,7 +749,7 @@ class FreqtradeBot(LoggingMixin):
trades_closed += 1
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
if trades_closed:
@ -742,33 +759,42 @@ class FreqtradeBot(LoggingMixin):
def handle_trade(self, trade: Trade) -> bool:
"""
Sells the current pair if the threshold is reached and updates the trade record.
:return: True if trade has been sold, False otherwise
Sells/exits_short the current pair if the threshold is reached and updates the trade record.
:return: True if trade has been sold/exited_short, False otherwise
"""
if not trade.is_open:
raise DependencyException(f'Attempt to handle closed trade: {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
self.config.get('ignore_roi_if_buy_signal', False)):
if trade.is_short:
# 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,
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")
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
def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
@ -795,6 +821,11 @@ class FreqtradeBot(LoggingMixin):
except InvalidOrderException as e:
trade.stoploss_order_id = None
logger.error(f'Unable to place a stoploss order on exchange. {e}')
if trade.is_short:
logger.warning('Exiting(Buying) the trade forcefully')
# 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))
@ -809,6 +840,8 @@ class FreqtradeBot(LoggingMixin):
Check if trade is fulfilled in which case the stoploss
on exchange should be added immediately if stoploss on exchange
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)
@ -827,13 +860,15 @@ class FreqtradeBot(LoggingMixin):
# We check if stoploss order is fulfilled
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
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
stoploss_order=True)
# 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),
reason='Auto lock')
self._notify_sell(trade, "stoploss")
self._notify_close(trade, "stoploss")
return True
if trade.open_order_id or not trade.is_open:
@ -842,9 +877,12 @@ class FreqtradeBot(LoggingMixin):
# The trade can be closed already (sell-order fill confirmation came in this iteration)
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:
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)
if self.create_stoploss_order(trade=trade, stop_price=stop_price):
@ -906,6 +944,7 @@ class FreqtradeBot(LoggingMixin):
buy: bool, sell: bool) -> bool:
"""
Check and execute sell
# TODO-mg: Update this for shorts
"""
should_sell = self.strategy.should_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)
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
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,
default_retval=False)(pair=trade.pair,
trade=trade,
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 (
fully_cancelled
@ -982,13 +1022,13 @@ class FreqtradeBot(LoggingMixin):
continue
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':
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
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
:return: True if order was fully cancelled
@ -1005,7 +1045,7 @@ class FreqtradeBot(LoggingMixin):
if filled_val > 0 and filled_stake < minstake:
logger.warning(
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
corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
trade.amount)
@ -1020,12 +1060,19 @@ class FreqtradeBot(LoggingMixin):
corder = order
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(f'{side} order %s for %s.', reason, trade)
# Using filled to determine the filled amount
filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled')
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
trade.delete()
was_trade_fully_canceled = True
@ -1036,16 +1083,20 @@ class FreqtradeBot(LoggingMixin):
# cancel_order may not contain the full order dict, so we need to fallback
# to the order dict aquired before cancelling.
# 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.stake_amount = trade.amount * trade.open_rate
self.update_trade_state(trade, trade.open_order_id, corder)
trade.open_order_id = None
# TODO-mg change to buy or short order
logger.info('Partial buy order timeout for %s.', trade)
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
self.wallets.update()
self._notify_buy_cancel(trade, order_type=self.strategy.order_types['buy'],
# TODO-mg change to buy or short order
# 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
@ -1083,7 +1134,7 @@ class FreqtradeBot(LoggingMixin):
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
self.wallets.update()
self._notify_sell_cancel(
self._notify_close_cancel(
trade,
order_type=self.strategy.order_types['sell'],
reason=reason
@ -1123,6 +1174,7 @@ class FreqtradeBot(LoggingMixin):
:param sell_reason: Reason the sell was triggered
:return: True if it succeeds (supported) False (not supported)
"""
# TODO-mg: account for leverage and make this work for shorts
sell_type = 'sell'
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
sell_type = 'stoploss'
@ -1190,11 +1242,11 @@ class FreqtradeBot(LoggingMixin):
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
reason='Auto lock')
self._notify_sell(trade, order_type)
self._notify_close(trade, order_type)
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.
"""
@ -1236,7 +1288,7 @@ class FreqtradeBot(LoggingMixin):
# Send the message
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.
"""
@ -1331,13 +1383,13 @@ class FreqtradeBot(LoggingMixin):
# Updating wallets when order is closed
if not trade.is_open:
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.global_stop()
self.wallets.update()
elif not trade.open_order_id:
# Buy fill
self._notify_buy_fill(trade)
self._notify_open_fill(trade)
return False
@ -1350,6 +1402,7 @@ class FreqtradeBot(LoggingMixin):
self.wallets.update()
if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount:
# 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 - "
f"Eating Fee {fee_abs} into dust.")
elif fee_abs != 0:
@ -1361,6 +1414,7 @@ class FreqtradeBot(LoggingMixin):
def get_real_amount(self, trade: Trade, order: Dict) -> float:
"""
TODO-mg: Update this function to account for shorts
Detect and update trade fee.
Calls trade.update_fee() upon correct detection.
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:
"""
TODO-mg: Update this function to account for shorts
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),

View File

@ -36,6 +36,7 @@ class RPCException(Exception):
raise RPCException('*Status:* `no active trade`')
"""
# TODO-mg: Add new configuration options introduced with leveraged/short trading
def __init__(self, message: str) -> None:
super().__init__(self)
@ -545,7 +546,7 @@ class RPC:
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
if order['side'] == 'buy':
fully_canceled = self._freqtrade.handle_cancel_buy(
fully_canceled = self._freqtrade.handle_cancel_enter(
trade, order, CANCEL_REASON['FORCE_SELL'])
if order['side'] == 'sell':
@ -613,7 +614,7 @@ class RPC:
stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair)
# 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 = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
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)
result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC')
assert pytest.approx(result) == expected[i]
freqtrade.execute_buy('ETH/BTC', result)
freqtrade.execute_enter('ETH/BTC', result)
else:
with pytest.raises(DependencyException):
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)
# Create 2 existing trades
freqtrade.execute_buy('ETH/BTC', default_conf['stake_amount'])
freqtrade.execute_buy('NEO/BTC', default_conf['stake_amount'])
freqtrade.execute_enter('ETH/BTC', default_conf['stake_amount'])
freqtrade.execute_enter('NEO/BTC', default_conf['stake_amount'])
assert len(Trade.get_open_trades()) == 2
# 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]
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_exchange(mocker)
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
bid = 0.11
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(
'freqtrade.exchange.Exchange',
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'
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_mm.call_count == 0
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'
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_mm.call_count == 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
limit_buy_order_open['id'] = '33'
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
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',
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]
assert trade
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'
mocker.patch('freqtrade.exchange.Exchange.create_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]
assert trade
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'
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]
assert trade
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
limit_buy_order['id'] = '557'
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]
assert trade
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'
mocker.patch('freqtrade.exchange.Exchange.create_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...
mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(return_value=0.0))
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)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@ -923,18 +925,18 @@ def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -
pair = 'ETH/BTC'
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'
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'
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)
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:
@ -1686,7 +1688,7 @@ def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog)
)
n = freqtrade.exit_positions(trades)
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:
@ -2419,7 +2421,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
handle_cancel_buy=MagicMock(),
handle_cancel_enter=MagicMock(),
handle_cancel_sell=MagicMock(),
)
mocker.patch.multiple(
@ -2441,7 +2443,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke
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_exchange(mocker)
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)
freqtrade = FreqtradeBot(default_conf)
freqtrade._notify_buy_cancel = MagicMock()
freqtrade._notify_open_cancel = MagicMock()
trade = MagicMock()
trade.pair = 'LTC/USDT'
trade.open_rate = 200
trade.is_short = False
trade.enter_side = "buy"
limit_buy_order['filled'] = 0.0
limit_buy_order['status'] = 'open'
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
cancel_order_mock.reset_mock()
caplog.clear()
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 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()
cancel_order_mock.reset_mock()
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
# Order remained open for some reason (cancel failed)
cancel_buy_order['status'] = 'open'
cancel_order_mock = MagicMock(return_value=cancel_buy_order)
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)
@pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'],
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:
patch_RPCManager(mocker)
patch_exchange(mocker)
cancel_order_mock = mocker.patch(
'freqtrade.exchange.Exchange.cancel_order_with_result',
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)
reason = CANCEL_REASON['TIMEOUT']
trade = MagicMock()
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 log_has_re(r'Buy order fully cancelled. Removing .* from database\.', caplog)
assert nofiy_mock.call_count == 1
@ -2511,7 +2515,7 @@ def test_handle_cancel_buy_exchanges(mocker, caplog, default_conf,
'String Return value',
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:
patch_RPCManager(mocker)
patch_exchange(mocker)
@ -2522,7 +2526,7 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order,
)
freqtrade = FreqtradeBot(default_conf)
freqtrade._notify_buy_cancel = MagicMock()
freqtrade._notify_open_cancel = MagicMock()
trade = MagicMock()
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['status'] = 'open'
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
cancel_order_mock.reset_mock()
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
@ -4099,7 +4103,7 @@ def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None:
freqtrade = FreqtradeBot(default_conf)
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,
@ -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',
side_effect=[
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')
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(
'freqtrade.freqtradebot.FreqtradeBot',
create_stoploss_order=MagicMock(return_value=True),
_notify_sell=MagicMock(),
_notify_close=MagicMock(),
)
mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock)
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(
'freqtrade.freqtradebot.FreqtradeBot',
create_stoploss_order=MagicMock(return_value=True),
_notify_sell=MagicMock(),
_notify_close=MagicMock(),
)
should_sell_mock = MagicMock(side_effect=[
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
# 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')
assert result == result1
# 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')
assert result == 0