Merge branch 'freqtrade:develop' into plot_hyperopt_stats

This commit is contained in:
Italo 2022-03-11 17:36:00 +00:00 committed by GitHub
commit 1a573d57b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 132 additions and 53 deletions

View File

@ -23,10 +23,10 @@ jobs:
python-version: ["3.8", "3.9", "3.10"] python-version: ["3.8", "3.9", "3.10"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v3
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@ -118,10 +118,10 @@ jobs:
python-version: ["3.8", "3.9", "3.10"] python-version: ["3.8", "3.9", "3.10"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v3
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@ -210,10 +210,10 @@ jobs:
python-version: ["3.8", "3.9", "3.10"] python-version: ["3.8", "3.9", "3.10"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v3
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@ -262,14 +262,14 @@ jobs:
docs_check: docs_check:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Documentation syntax - name: Documentation syntax
run: | run: |
./tests/test_docs.sh ./tests/test_docs.sh
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v3
with: with:
python-version: 3.8 python-version: 3.8
@ -325,10 +325,10 @@ jobs:
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade' if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v3
with: with:
python-version: 3.8 python-version: 3.8
@ -405,7 +405,7 @@ jobs:
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade' if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Extract branch name - name: Extract branch name
shell: bash shell: bash

View File

@ -8,7 +8,7 @@ jobs:
dockerHubDescription: dockerHubDescription:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v3
- name: Docker Hub Description - name: Docker Hub Description
uses: peter-evans/dockerhub-description@v2.4.3 uses: peter-evans/dockerhub-description@v2.4.3
env: env:

View File

@ -32,7 +32,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
- [X] [Binance](https://www.binance.com/) - [X] [Binance](https://www.binance.com/)
- [X] [Bittrex](https://bittrex.com/) - [X] [Bittrex](https://bittrex.com/)
- [X] [FTX](https://ftx.com) - [X] [FTX](https://ftx.com/#a=2258149)
- [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [Huobi](http://huobi.com/) - [X] [Huobi](http://huobi.com/)
- [X] [Kraken](https://kraken.com/) - [X] [Kraken](https://kraken.com/)

View File

@ -56,7 +56,8 @@
"forcebuy": "market", "forcebuy": "market",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": false, "stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60 "stoploss_on_exchange_interval": 60,
"stoploss_on_exchange_limit_ratio": 0.99
}, },
"order_time_in_force": { "order_time_in_force": {
"buy": "gtc", "buy": "gtc",

View File

@ -44,7 +44,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
- [X] [Binance](https://www.binance.com/) - [X] [Binance](https://www.binance.com/)
- [X] [Bittrex](https://bittrex.com/) - [X] [Bittrex](https://bittrex.com/)
- [X] [FTX](https://ftx.com) - [X] [FTX](https://ftx.com/#a=2258149)
- [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [Huobi](http://huobi.com/) - [X] [Huobi](http://huobi.com/)
- [X] [Kraken](https://kraken.com/) - [X] [Kraken](https://kraken.com/)

View File

@ -1,4 +1,4 @@
mkdocs==1.2.3 mkdocs==1.2.3
mkdocs-material==8.2.3 mkdocs-material==8.2.5
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2
pymdown-extensions==9.2 pymdown-extensions==9.2

View File

@ -140,7 +140,7 @@ CONF_SCHEMA = {
'minProperties': 1 'minProperties': 1
}, },
'amount_reserve_percent': {'type': 'number', 'minimum': 0.0, 'maximum': 0.5}, 'amount_reserve_percent': {'type': 'number', 'minimum': 0.0, 'maximum': 0.5},
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True}, 'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True, 'minimum': -1},
'trailing_stop': {'type': 'boolean'}, 'trailing_stop': {'type': 'boolean'},
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1}, 'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1}, 'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},

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))
@ -907,7 +911,9 @@ class FreqtradeBot(LoggingMixin):
:param order: Current on exchange stoploss order :param order: Current on exchange stoploss order
:return: None :return: None
""" """
if self.exchange.stoploss_adjust(trade.stop_loss, order): stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stop_loss)
if self.exchange.stoploss_adjust(stoploss_norm, order):
# we check if the update is necessary # we check if the update is necessary
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:

View File

@ -304,7 +304,7 @@ class LocalTrade():
# absolute value of the initial stop loss # absolute value of the initial stop loss
initial_stop_loss: float = 0.0 initial_stop_loss: float = 0.0
# percentage value of the initial stop loss # percentage value of the initial stop loss
initial_stop_loss_pct: float = 0.0 initial_stop_loss_pct: Optional[float] = None
# stoploss order id which is on exchange # stoploss order id which is on exchange
stoploss_order_id: Optional[str] = None stoploss_order_id: Optional[str] = None
# last update time of the stoploss order on exchange # last update time of the stoploss order on exchange
@ -446,7 +446,8 @@ class LocalTrade():
new_loss = float(current_price * (1 - abs(stoploss))) new_loss = float(current_price * (1 - abs(stoploss)))
# no stop loss assigned yet # no stop loss assigned yet
if not self.stop_loss: # if not self.stop_loss:
if self.initial_stop_loss_pct is None:
logger.debug(f"{self.pair} - Assigning new stoploss...") logger.debug(f"{self.pair} - Assigning new stoploss...")
self._set_new_stoploss(new_loss, stoploss) self._set_new_stoploss(new_loss, stoploss)
self.initial_stop_loss = new_loss self.initial_stop_loss = new_loss
@ -786,6 +787,7 @@ class LocalTrade():
logger.info(f"Stoploss for {trade} needs adjustment...") logger.info(f"Stoploss for {trade} needs adjustment...")
# Force reset of stoploss # Force reset of stoploss
trade.stop_loss = None trade.stop_loss = None
trade.initial_stop_loss_pct = None
trade.adjust_stop_loss(trade.open_rate, desired_stoploss) trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
logger.info(f"New stoploss: {trade.stop_loss}.") logger.info(f"New stoploss: {trade.stop_loss}.")

View File

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

View File

@ -8,7 +8,7 @@ flake8==4.0.1
flake8-tidy-imports==4.6.0 flake8-tidy-imports==4.6.0
mypy==0.931 mypy==0.931
pytest==7.0.1 pytest==7.0.1
pytest-asyncio==0.18.1 pytest-asyncio==0.18.2
pytest-cov==3.0.0 pytest-cov==3.0.0
pytest-mock==3.7.0 pytest-mock==3.7.0
pytest-random-order==1.0.4 pytest-random-order==1.0.4
@ -20,7 +20,7 @@ time-machine==2.6.0
nbconvert==6.4.2 nbconvert==6.4.2
# mypy types # mypy types
types-cachetools==4.2.9 types-cachetools==4.2.10
types-filelock==3.2.5 types-filelock==3.2.5
types-requests==2.27.11 types-requests==2.27.11
types-tabulate==0.8.5 types-tabulate==0.8.5

View File

@ -2,11 +2,11 @@ numpy==1.22.2
pandas==1.4.1 pandas==1.4.1
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==1.74.63 ccxt==1.75.12
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==36.0.1 cryptography==36.0.1
aiohttp==3.8.1 aiohttp==3.8.1
SQLAlchemy==1.4.31 SQLAlchemy==1.4.32
python-telegram-bot==13.11 python-telegram-bot==13.11
arrow==1.2.2 arrow==1.2.2
cachetools==4.2.2 cachetools==4.2.2
@ -31,7 +31,7 @@ python-rapidjson==1.6
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.74.1 fastapi==0.75.0
uvicorn==0.17.5 uvicorn==0.17.5
pyjwt==2.3.0 pyjwt==2.3.0
aiofiles==0.8.0 aiofiles==0.8.0

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:
@ -1360,6 +1385,32 @@ def test_handle_stoploss_on_exchange_trailing_error(
assert log_has_re(r"Could not create trailing stoploss order for pair ETH/USDT\..*", caplog) assert log_has_re(r"Could not create trailing stoploss order for pair ETH/USDT\..*", caplog)
def test_stoploss_on_exchange_price_rounding(
mocker, default_conf_usdt, fee, open_trade_usdt) -> None:
patch_RPCManager(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_fee=fee,
)
price_mock = MagicMock(side_effect=lambda p, s: int(s))
stoploss_mock = MagicMock(return_value={'id': '13434334'})
adjust_mock = MagicMock(return_value=False)
mocker.patch.multiple(
'freqtrade.exchange.Binance',
stoploss=stoploss_mock,
stoploss_adjust=adjust_mock,
price_to_precision=price_mock,
)
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
open_trade_usdt.stoploss_order_id = '13434334'
open_trade_usdt.stop_loss = 222.55
freqtrade.handle_trailing_stoploss_on_exchange(open_trade_usdt, {})
assert price_mock.call_count == 1
assert adjust_mock.call_count == 1
assert adjust_mock.call_args_list[0][0][0] == 222
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_handle_stoploss_on_exchange_custom_stop( def test_handle_stoploss_on_exchange_custom_stop(
mocker, default_conf_usdt, fee, limit_buy_order_usdt, limit_sell_order_usdt) -> None: mocker, default_conf_usdt, fee, limit_buy_order_usdt, limit_sell_order_usdt) -> None: