mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-14 04:03:55 +00:00
Merge pull request #6464 from freqtrade/new_release
New release 2022.2.1
This commit is contained in:
commit
a568548192
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -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
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = '2022.2'
|
__version__ = '2022.2.1'
|
||||||
|
|
||||||
if __version__ == 'develop':
|
if __version__ == 'develop':
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
if prev_avg_price:
|
||||||
minus_on_entry = (cur_entry_average - prev_avg_price) / 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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user