mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge branch 'develop' into gateio-stoploss
This commit is contained in:
commit
bf5afbcdbd
|
@ -9,7 +9,7 @@ import logging
|
|||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import ceil
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Any, Coroutine, Dict, List, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
import ccxt
|
||||
|
@ -1371,6 +1371,22 @@ class Exchange:
|
|||
data = sorted(data, key=lambda x: x[0])
|
||||
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, *,
|
||||
since_ms: Optional[int] = None, cache: bool = True
|
||||
) -> Dict[Tuple[str, str], DataFrame]:
|
||||
|
@ -1389,22 +1405,15 @@ class Exchange:
|
|||
cached_pairs = []
|
||||
# Gather coroutines to run
|
||||
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
|
||||
or self._now_is_time_to_refresh(pair, timeframe)):
|
||||
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:
|
||||
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))
|
||||
input_coroutines.append(self._build_coroutine(pair, timeframe, since_ms))
|
||||
else:
|
||||
logger.debug(
|
||||
"Using cached candle (OHLCV) data for pair %s, timeframe %s ...",
|
||||
|
|
|
@ -873,11 +873,15 @@ class FreqtradeBot(LoggingMixin):
|
|||
stop_price = trade.open_rate * (1 + stoploss)
|
||||
|
||||
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()
|
||||
return False
|
||||
|
||||
# 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):
|
||||
return False
|
||||
else:
|
||||
|
@ -887,7 +891,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
# 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
|
||||
if (
|
||||
stoploss_order
|
||||
trade.is_open and stoploss_order
|
||||
and stoploss_order.get('status_stop') != 'triggered'
|
||||
and (self.config.get('trailing_stop', False)
|
||||
or self.config.get('use_custom_stoploss', False))
|
||||
|
|
|
@ -23,6 +23,7 @@ coingecko_mapping = {
|
|||
'eth': 'ethereum',
|
||||
'bnb': 'binancecoin',
|
||||
'sol': 'solana',
|
||||
'usdt': 'tether',
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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))
|
||||
else:
|
||||
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',
|
||||
|
|
|
@ -1692,6 +1692,13 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
|||
cache=False)
|
||||
assert len(res) == 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
|
||||
|
|
|
@ -926,12 +926,10 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog,
|
|||
}),
|
||||
create_order=MagicMock(side_effect=[
|
||||
{'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,
|
||||
)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Binance',
|
||||
stoploss=stoploss
|
||||
)
|
||||
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
|
||||
|
||||
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 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
|
||||
|
||||
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()
|
||||
|
||||
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,
|
||||
'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 log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog)
|
||||
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()
|
||||
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Binance.stoploss',
|
||||
'freqtrade.exchange.Exchange.stoploss',
|
||||
side_effect=ExchangeError()
|
||||
)
|
||||
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
|
||||
trade.stoploss_order_id = 100
|
||||
stoploss.reset_mock()
|
||||
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order',
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order',
|
||||
side_effect=InvalidOrderException())
|
||||
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
|
||||
freqtrade.handle_stoploss_on_exchange(trade)
|
||||
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
|
||||
stoploss.reset_mock()
|
||||
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 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,
|
||||
limit_buy_order_usdt, limit_sell_order_usdt) -> None:
|
||||
|
|
Loading…
Reference in New Issue
Block a user