Merge branch 'freqtrade:develop' into plot_hyperopt_stats
This commit is contained in:
commit
1a573d57b9
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@ -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
|
||||||
|
2
.github/workflows/docker_update_readme.yml
vendored
2
.github/workflows/docker_update_readme.yml
vendored
@ -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:
|
||||||
|
@ -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/)
|
||||||
|
@ -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",
|
||||||
|
@ -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/)
|
||||||
|
@ -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
|
||||||
|
@ -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},
|
||||||
|
@ -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 ...",
|
||||||
|
@ -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:
|
||||||
|
@ -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}.")
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ coingecko_mapping = {
|
|||||||
'eth': 'ethereum',
|
'eth': 'ethereum',
|
||||||
'bnb': 'binancecoin',
|
'bnb': 'binancecoin',
|
||||||
'sol': 'solana',
|
'sol': 'solana',
|
||||||
|
'usdt': 'tether',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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',
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user