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"]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
@ -118,10 +118,10 @@ jobs:
python-version: ["3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
@ -210,10 +210,10 @@ jobs:
python-version: ["3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
@ -262,14 +262,14 @@ jobs:
docs_check:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Documentation syntax
run: |
./tests/test_docs.sh
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v3
with:
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'
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v3
with:
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'
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Extract branch name
shell: bash

View File

@ -8,7 +8,7 @@ jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v3
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v2.4.3
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] [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] [Huobi](http://huobi.com/)
- [X] [Kraken](https://kraken.com/)

View File

@ -56,7 +56,8 @@
"forcebuy": "market",
"stoploss": "market",
"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": {
"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] [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] [Huobi](http://huobi.com/)
- [X] [Kraken](https://kraken.com/)

View File

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

View File

@ -140,7 +140,7 @@ CONF_SCHEMA = {
'minProperties': 1
},
'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_positive': {'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 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 ...",

View File

@ -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))
@ -907,7 +911,9 @@ class FreqtradeBot(LoggingMixin):
:param order: Current on exchange stoploss order
: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
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
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
initial_stop_loss: float = 0.0
# 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: Optional[str] = None
# last update time of the stoploss order on exchange
@ -446,7 +446,8 @@ class LocalTrade():
new_loss = float(current_price * (1 - abs(stoploss)))
# 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...")
self._set_new_stoploss(new_loss, stoploss)
self.initial_stop_loss = new_loss
@ -786,6 +787,7 @@ class LocalTrade():
logger.info(f"Stoploss for {trade} needs adjustment...")
# Force reset of stoploss
trade.stop_loss = None
trade.initial_stop_loss_pct = None
trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
logger.info(f"New stoploss: {trade.stop_loss}.")

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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:
@ -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)
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")
def test_handle_stoploss_on_exchange_custom_stop(
mocker, default_conf_usdt, fee, limit_buy_order_usdt, limit_sell_order_usdt) -> None: