Merge pull request #6464 from freqtrade/new_release

New release 2022.2.1
This commit is contained in:
Matthias 2022-02-26 08:57:42 +01:00 committed by GitHub
commit a568548192
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 58 additions and 37 deletions

2
.gitignore vendored
View File

@ -1,6 +1,8 @@
# Freqtrade rules # Freqtrade rules
config*.json config*.json
*.sqlite *.sqlite
*.sqlite-shm
*.sqlite-wal
logfile.txt logfile.txt
user_data/* user_data/*
!user_data/strategy/sample_strategy.py !user_data/strategy/sample_strategy.py

View File

@ -1,5 +1,5 @@
""" Freqtrade bot """ """ Freqtrade bot """
__version__ = '2022.2' __version__ = '2022.2.1'
if __version__ == 'develop': if __version__ == 'develop':

View File

@ -979,10 +979,10 @@ class FreqtradeBot(LoggingMixin):
or (order_obj and self.strategy.ft_check_timed_out( or (order_obj and self.strategy.ft_check_timed_out(
'sell', trade, order_obj, datetime.now(timezone.utc)) 'sell', trade, order_obj, datetime.now(timezone.utc))
))): ))):
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) canceled = self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
canceled_count = trade.get_exit_order_count() canceled_count = trade.get_exit_order_count()
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
if max_timeouts > 0 and canceled_count >= max_timeouts: if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
logger.warning(f'Emergencyselling trade {trade}, as the sell order ' logger.warning(f'Emergencyselling trade {trade}, as the sell order '
f'timed out {max_timeouts} times.') f'timed out {max_timeouts} times.')
try: try:
@ -1079,11 +1079,12 @@ class FreqtradeBot(LoggingMixin):
reason=reason) reason=reason)
return was_trade_fully_canceled return was_trade_fully_canceled
def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str: def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> bool:
""" """
Sell cancel - cancel order and update trade Sell cancel - cancel order and update trade
:return: Reason for cancel :return: True if exit order was cancelled, false otherwise
""" """
cancelled = False
# if trade is not partially completed, just cancel the order # if trade is not partially completed, just cancel the order
if order['remaining'] == order['amount'] or order.get('filled') == 0.0: if order['remaining'] == order['amount'] or order.get('filled') == 0.0:
if not self.exchange.check_order_canceled_empty(order): if not self.exchange.check_order_canceled_empty(order):
@ -1094,7 +1095,7 @@ class FreqtradeBot(LoggingMixin):
trade.update_order(co) trade.update_order(co)
except InvalidOrderException: except InvalidOrderException:
logger.exception(f"Could not cancel sell order {trade.open_order_id}") logger.exception(f"Could not cancel sell order {trade.open_order_id}")
return 'error cancelling order' return False
logger.info('Sell order %s for %s.', reason, trade) logger.info('Sell order %s for %s.', reason, trade)
else: else:
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
@ -1108,9 +1109,11 @@ class FreqtradeBot(LoggingMixin):
trade.close_date = None trade.close_date = None
trade.is_open = True trade.is_open = True
trade.open_order_id = None trade.open_order_id = None
cancelled = True
else: else:
# TODO: figure out how to handle partially complete sell orders # TODO: figure out how to handle partially complete sell orders
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
cancelled = False
self.wallets.update() self.wallets.update()
self._notify_exit_cancel( self._notify_exit_cancel(
@ -1118,7 +1121,7 @@ class FreqtradeBot(LoggingMixin):
order_type=self.strategy.order_types['sell'], order_type=self.strategy.order_types['sell'],
reason=reason reason=reason
) )
return reason return cancelled
def _safe_exit_amount(self, pair: str, amount: float) -> float: def _safe_exit_amount(self, pair: str, amount: float) -> float:
""" """

View File

@ -195,6 +195,7 @@ class Order(_DECL_BASE):
return { return {
'amount': self.amount, 'amount': self.amount,
'average': round(self.average, 8) if self.average else 0, 'average': round(self.average, 8) if self.average else 0,
'safe_price': self.safe_price,
'cost': self.cost if self.cost else 0, 'cost': self.cost if self.cost else 0,
'filled': self.filled, 'filled': self.filled,
'ft_order_side': self.ft_order_side, 'ft_order_side': self.ft_order_side,

View File

@ -370,46 +370,50 @@ class Telegram(RPCHandler):
else: else:
return "\N{CROSS MARK}" return "\N{CROSS MARK}"
def _prepare_entry_details(self, filled_orders, base_currency, is_open): def _prepare_entry_details(self, filled_orders: List, base_currency: str, is_open: bool):
""" """
Prepare details of trade with entry adjustment enabled Prepare details of trade with entry adjustment enabled
""" """
lines = [] lines: List[str] = []
if len(filled_orders) > 0:
first_avg = filled_orders[0]["safe_price"]
for x, order in enumerate(filled_orders): for x, order in enumerate(filled_orders):
cur_entry_datetime = arrow.get(order["order_filled_date"]) cur_entry_datetime = arrow.get(order["order_filled_date"])
cur_entry_amount = order["amount"] cur_entry_amount = order["amount"]
cur_entry_average = order["average"] cur_entry_average = order["safe_price"]
lines.append(" ") lines.append(" ")
if x == 0: if x == 0:
lines.append("*Entry #{}:*".format(x+1)) lines.append(f"*Entry #{x+1}:*")
lines.append("*Entry Amount:* {} ({:.8f} {})" lines.append(
.format(cur_entry_amount, order["cost"], base_currency)) f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
lines.append("*Average Entry Price:* {}".format(cur_entry_average)) lines.append(f"*Average Entry Price:* {cur_entry_average}")
else: else:
sumA = 0 sumA = 0
sumB = 0 sumB = 0
for y in range(x): for y in range(x):
sumA += (filled_orders[y]["amount"] * filled_orders[y]["average"]) sumA += (filled_orders[y]["amount"] * filled_orders[y]["safe_price"])
sumB += filled_orders[y]["amount"] sumB += filled_orders[y]["amount"]
prev_avg_price = sumA/sumB prev_avg_price = sumA / sumB
price_to_1st_entry = ((cur_entry_average - filled_orders[0]["average"]) price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg)
/ filled_orders[0]["average"]) minus_on_entry = 0
minus_on_entry = (cur_entry_average - prev_avg_price)/prev_avg_price if prev_avg_price:
minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price
dur_entry = cur_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"]) dur_entry = cur_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"])
days = dur_entry.days days = dur_entry.days
hours, remainder = divmod(dur_entry.seconds, 3600) hours, remainder = divmod(dur_entry.seconds, 3600)
minutes, seconds = divmod(remainder, 60) minutes, seconds = divmod(remainder, 60)
lines.append("*Entry #{}:* at {:.2%} avg profit".format(x+1, minus_on_entry)) lines.append(f"*Entry #{x+1}:* at {minus_on_entry:.2%} avg profit")
if is_open: if is_open:
lines.append("({})".format(cur_entry_datetime lines.append("({})".format(cur_entry_datetime
.humanize(granularity=["day", "hour", "minute"]))) .humanize(granularity=["day", "hour", "minute"])))
lines.append("*Entry Amount:* {} ({:.8f} {})" lines.append(
.format(cur_entry_amount, order["cost"], base_currency)) f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
lines.append("*Average Entry Price:* {} ({:.2%} from 1st entry rate)" lines.append(f"*Average Entry Price:* {cur_entry_average} "
.format(cur_entry_average, price_to_1st_entry)) f"({price_to_1st_entry:.2%} from 1st entry rate)")
lines.append("*Order filled at:* {}".format(order["order_filled_date"])) lines.append(f"*Order filled at:* {order['order_filled_date']}")
lines.append("({}d {}h {}m {}s from previous entry)" lines.append(f"({days}d {hours}h {minutes}m {seconds}s from previous entry)")
.format(days, hours, minutes, seconds))
return lines return lines
@authorized_only @authorized_only

View File

@ -110,7 +110,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'open_order': None, 'open_order': None,
'exchange': 'binance', 'exchange': 'binance',
'filled_entry_orders': [{ 'filled_entry_orders': [{
'amount': 91.07468123, 'average': 1.098e-05, 'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY, 'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05, 'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
@ -185,7 +185,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'open_order': None, 'open_order': None,
'exchange': 'binance', 'exchange': 'binance',
'filled_entry_orders': [{ 'filled_entry_orders': [{
'amount': 91.07468123, 'average': 1.098e-05, 'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY, 'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05, 'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,

View File

@ -236,6 +236,8 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
create_mock_trades(fee) create_mock_trades(fee)
trades = Trade.get_open_trades() trades = Trade.get_open_trades()
trade = trades[0] trade = trades[0]
# Average may be empty on some exchanges
trade.orders[0].average = 0
trade.orders.append(Order( trade.orders.append(Order(
order_id='5412vbb', order_id='5412vbb',
ft_order_side='buy', ft_order_side='buy',
@ -246,7 +248,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
order_type="market", order_type="market",
side="buy", side="buy",
price=trade.open_rate * 0.95, price=trade.open_rate * 0.95,
average=trade.open_rate * 0.95, average=0,
filled=trade.amount, filled=trade.amount,
remaining=0, remaining=0,
cost=trade.amount, cost=trade.amount,

View File

@ -6,7 +6,7 @@ import time
from copy import deepcopy from copy import deepcopy
from math import isclose from math import isclose
from typing import List from typing import List
from unittest.mock import ANY, MagicMock, PropertyMock from unittest.mock import ANY, MagicMock, PropertyMock, patch
import arrow import arrow
import pytest import pytest
@ -2220,9 +2220,14 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l
et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit') et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit')
caplog.clear() caplog.clear()
# 2nd canceled trade ... # 2nd canceled trade ...
open_trade.open_order_id = limit_sell_order_old['id'] open_trade.open_order_id = limit_sell_order_old['id']
# If cancelling fails - no emergency sell!
with patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit', return_value=False):
freqtrade.check_handle_timedout()
assert et_mock.call_count == 0
freqtrade.check_handle_timedout() freqtrade.check_handle_timedout()
assert log_has_re('Emergencyselling trade.*', caplog) assert log_has_re('Emergencyselling trade.*', caplog)
assert et_mock.call_count == 1 assert et_mock.call_count == 1
@ -2564,13 +2569,17 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
send_msg_mock.reset_mock() send_msg_mock.reset_mock()
order['amount'] = 2 order['amount'] = 2
assert freqtrade.handle_cancel_exit(trade, order, reason assert not freqtrade.handle_cancel_exit(trade, order, reason)
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
# Assert cancel_order was not called (callcount remains unchanged) # Assert cancel_order was not called (callcount remains unchanged)
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert send_msg_mock.call_count == 1 assert send_msg_mock.call_count == 1
assert freqtrade.handle_cancel_exit(trade, order, reason assert (send_msg_mock.call_args_list[0][0][0]['reason']
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'])
assert not freqtrade.handle_cancel_exit(trade, order, reason)
send_msg_mock.call_args_list[0][0][0]['reason'] = CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
# Message should not be iterated again # Message should not be iterated again
assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
assert send_msg_mock.call_count == 1 assert send_msg_mock.call_count == 1
@ -2589,7 +2598,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
order = {'remaining': 1, order = {'remaining': 1,
'amount': 1, 'amount': 1,
'status': "open"} 'status': "open"}
assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order' assert not freqtrade.handle_cancel_exit(trade, order, reason)
def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, mocker def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, mocker