finished adding TODOs to freqtradebot
This commit is contained in:
parent
13c8ee9371
commit
3671a8aa13
@ -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
|
||||
|
||||
|
@ -7,6 +7,8 @@ class SignalType(Enum):
|
||||
"""
|
||||
BUY = "buy"
|
||||
SELL = "sell"
|
||||
SHORT = "short"
|
||||
EXIT_SHORT = "exit_short"
|
||||
|
||||
|
||||
class SignalTagType(Enum):
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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', {})
|
||||
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
|
||||
strat = 'bid_strategy'
|
||||
side = 'buy'
|
||||
# execute_trade = self.execute_enter
|
||||
lev = long_lev
|
||||
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)
|
||||
else:
|
||||
return False
|
||||
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(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,9 +821,14 @@ class FreqtradeBot(LoggingMixin):
|
||||
except InvalidOrderException as e:
|
||||
trade.stoploss_order_id = None
|
||||
logger.error(f'Unable to place a stoploss order on exchange. {e}')
|
||||
logger.warning('Selling the trade forcefully')
|
||||
self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple(
|
||||
sell_type=SellType.EMERGENCY_SELL))
|
||||
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))
|
||||
|
||||
except ExchangeError:
|
||||
trade.stoploss_order_id = None
|
||||
@ -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,10 +877,13 @@ 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
|
||||
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):
|
||||
trade.stoploss_last_update = datetime.utcnow()
|
||||
@ -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,17 +1083,21 @@ 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'],
|
||||
reason=reason)
|
||||
# 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
|
||||
|
||||
def handle_cancel_sell(self, trade: Trade, order: Dict, reason: str) -> str:
|
||||
@ -1083,7 +1134,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||
|
||||
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),
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
limit_buy_order_canceled_empty) -> None:
|
||||
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,8 +2515,8 @@ 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,
|
||||
cancelorder) -> None:
|
||||
def test_handle_cancel_enter_corder_empty(mocker, default_conf, limit_buy_order,
|
||||
cancelorder) -> None:
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
cancel_order_mock = MagicMock(return_value=cancelorder)
|
||||
@ -2522,7 +2526,7 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order,
|
||||
)
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
freqtrade._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)
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user