Merge branch 'develop' into gateio-stoploss

This commit is contained in:
Sam Germain 2022-03-12 19:50:46 -06:00
commit bf5afbcdbd
6 changed files with 76 additions and 28 deletions

View File

@ -9,7 +9,7 @@ import logging
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from math import ceil from math import ceil
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Coroutine, Dict, List, Optional, Tuple
import arrow import arrow
import ccxt import ccxt
@ -1371,6 +1371,22 @@ class Exchange:
data = sorted(data, key=lambda x: x[0]) data = sorted(data, key=lambda x: x[0])
return pair, timeframe, data return pair, timeframe, data
def _build_coroutine(self, pair: str, timeframe: str, since_ms: Optional[int]) -> Coroutine:
if not since_ms and self.required_candle_call_count > 1:
# Multiple calls for one pair - to get more history
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
move_to = one_call * self.required_candle_call_count
now = timeframe_to_next_date(timeframe)
since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000)
if since_ms:
return self._async_get_historic_ohlcv(
pair, timeframe, since_ms=since_ms, raise_=True)
else:
# One call ... "regular" refresh
return self._async_get_candle_history(
pair, timeframe, since_ms=since_ms)
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *, def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
since_ms: Optional[int] = None, cache: bool = True since_ms: Optional[int] = None, cache: bool = True
) -> Dict[Tuple[str, str], DataFrame]: ) -> Dict[Tuple[str, str], DataFrame]:
@ -1389,22 +1405,15 @@ class Exchange:
cached_pairs = [] cached_pairs = []
# Gather coroutines to run # Gather coroutines to run
for pair, timeframe in set(pair_list): for pair, timeframe in set(pair_list):
if timeframe not in self.timeframes:
logger.warning(
f"Cannot download ({pair}, {timeframe}) combination as this timeframe is "
f"not available on {self.name}. Available timeframes are "
f"{', '.join(self.timeframes)}.")
continue
if ((pair, timeframe) not in self._klines or not cache if ((pair, timeframe) not in self._klines or not cache
or self._now_is_time_to_refresh(pair, timeframe)): or self._now_is_time_to_refresh(pair, timeframe)):
if not since_ms and self.required_candle_call_count > 1: input_coroutines.append(self._build_coroutine(pair, timeframe, since_ms))
# Multiple calls for one pair - to get more history
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
move_to = one_call * self.required_candle_call_count
now = timeframe_to_next_date(timeframe)
since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000)
if since_ms:
input_coroutines.append(self._async_get_historic_ohlcv(
pair, timeframe, since_ms=since_ms, raise_=True))
else:
# One call ... "regular" refresh
input_coroutines.append(self._async_get_candle_history(
pair, timeframe, since_ms=since_ms))
else: else:
logger.debug( logger.debug(
"Using cached candle (OHLCV) data for pair %s, timeframe %s ...", "Using cached candle (OHLCV) data for pair %s, timeframe %s ...",

View File

@ -873,11 +873,15 @@ class FreqtradeBot(LoggingMixin):
stop_price = trade.open_rate * (1 + stoploss) stop_price = trade.open_rate * (1 + stoploss)
if self.create_stoploss_order(trade=trade, stop_price=stop_price): if self.create_stoploss_order(trade=trade, stop_price=stop_price):
# The above will return False if the placement failed and the trade was force-sold.
# in which case the trade will be closed - which we must check below.
trade.stoploss_last_update = datetime.utcnow() trade.stoploss_last_update = datetime.utcnow()
return False return False
# If stoploss order is canceled for some reason we add it # If stoploss order is canceled for some reason we add it
if stoploss_order and stoploss_order['status'] in ('canceled', 'cancelled'): if (trade.is_open
and stoploss_order
and stoploss_order['status'] in ('canceled', 'cancelled')):
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss): if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss):
return False return False
else: else:
@ -887,7 +891,7 @@ class FreqtradeBot(LoggingMixin):
# Finally we check if stoploss on exchange should be moved up because of trailing. # Finally we check if stoploss on exchange should be moved up because of trailing.
# Triggered Orders are now real orders - so don't replace stoploss anymore # Triggered Orders are now real orders - so don't replace stoploss anymore
if ( if (
stoploss_order trade.is_open and stoploss_order
and stoploss_order.get('status_stop') != 'triggered' and stoploss_order.get('status_stop') != 'triggered'
and (self.config.get('trailing_stop', False) and (self.config.get('trailing_stop', False)
or self.config.get('use_custom_stoploss', False)) or self.config.get('use_custom_stoploss', False))

View File

@ -23,6 +23,7 @@ coingecko_mapping = {
'eth': 'ethereum', 'eth': 'ethereum',
'bnb': 'binancecoin', 'bnb': 'binancecoin',
'sol': 'solana', 'sol': 'solana',
'usdt': 'tether',
} }

View File

@ -107,6 +107,8 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
else: else:
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock()) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.timeframes', PropertyMock(
return_value=['5m', '15m', '1h', '1d']))
def get_patched_exchange(mocker, config, api_mock=None, id='binance', def get_patched_exchange(mocker, config, api_mock=None, id='binance',

View File

@ -1692,6 +1692,13 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
cache=False) cache=False)
assert len(res) == 3 assert len(res) == 3
assert exchange._api_async.fetch_ohlcv.call_count == 3 assert exchange._api_async.fetch_ohlcv.call_count == 3
exchange._api_async.fetch_ohlcv.reset_mock()
caplog.clear()
# Call with invalid timeframe
res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '3m')], cache=False)
assert not res
assert len(res) == 0
assert log_has_re(r'Cannot download \(IOTA\/ETH, 3m\).*', caplog)
@pytest.mark.asyncio @pytest.mark.asyncio

View File

@ -926,12 +926,10 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog,
}), }),
create_order=MagicMock(side_effect=[ create_order=MagicMock(side_effect=[
{'id': limit_buy_order_usdt['id']}, {'id': limit_buy_order_usdt['id']},
{'id': limit_sell_order_usdt['id']}, limit_sell_order_usdt,
# {'id': limit_sell_order_usdt['id']},
]), ]),
get_fee=fee, get_fee=fee,
)
mocker.patch.multiple(
'freqtrade.exchange.Binance',
stoploss=stoploss stoploss=stoploss
) )
freqtrade = FreqtradeBot(default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt)
@ -956,7 +954,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog,
trade.stoploss_order_id = 100 trade.stoploss_order_id = 100
hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) hanging_stoploss_order = MagicMock(return_value={'status': 'open'})
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', hanging_stoploss_order) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order)
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.stoploss_order_id == 100 assert trade.stoploss_order_id == 100
@ -969,7 +967,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog,
trade.stoploss_order_id = 100 trade.stoploss_order_id = 100
canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'})
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', canceled_stoploss_order) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order)
stoploss.reset_mock() stoploss.reset_mock()
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
@ -1001,7 +999,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog,
'average': 2, 'average': 2,
'amount': limit_buy_order_usdt['amount'], 'amount': limit_buy_order_usdt['amount'],
}) })
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hit) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hit)
assert freqtrade.handle_stoploss_on_exchange(trade) is True assert freqtrade.handle_stoploss_on_exchange(trade) is True
assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog) assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog)
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
@ -1009,7 +1007,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog,
caplog.clear() caplog.clear()
mocker.patch( mocker.patch(
'freqtrade.exchange.Binance.stoploss', 'freqtrade.exchange.Exchange.stoploss',
side_effect=ExchangeError() side_effect=ExchangeError()
) )
trade.is_open = True trade.is_open = True
@ -1021,9 +1019,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog,
# It should try to add stoploss order # It should try to add stoploss order
trade.stoploss_order_id = 100 trade.stoploss_order_id = 100
stoploss.reset_mock() stoploss.reset_mock()
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order',
side_effect=InvalidOrderException()) side_effect=InvalidOrderException())
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
freqtrade.handle_stoploss_on_exchange(trade) freqtrade.handle_stoploss_on_exchange(trade)
assert stoploss.call_count == 1 assert stoploss.call_count == 1
@ -1033,10 +1031,37 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog,
trade.is_open = False trade.is_open = False
stoploss.reset_mock() stoploss.reset_mock()
mocker.patch('freqtrade.exchange.Exchange.fetch_order') mocker.patch('freqtrade.exchange.Exchange.fetch_order')
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert stoploss.call_count == 0 assert stoploss.call_count == 0
# Seventh case: emergency exit triggered
# Trailing stop should not act anymore
stoploss_order_cancelled = MagicMock(side_effect=[{
'id': "100",
'status': 'canceled',
'type': 'stop_loss_limit',
'price': 3,
'average': 2,
'amount': limit_buy_order_usdt['amount'],
'info': {'stopPrice': 22},
}])
trade.stoploss_order_id = 100
trade.is_open = True
trade.stoploss_last_update = arrow.utcnow().shift(hours=-1).datetime
trade.stop_loss = 24
freqtrade.config['trailing_stop'] = True
stoploss = MagicMock(side_effect=InvalidOrderException())
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order_with_result',
side_effect=InvalidOrderException())
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_cancelled)
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.stoploss_order_id is None
assert trade.is_open is False
assert trade.sell_reason == str(SellType.EMERGENCY_SELL)
def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
limit_buy_order_usdt, limit_sell_order_usdt) -> None: limit_buy_order_usdt, limit_sell_order_usdt) -> None: