Merge branch 'develop' into feat/pairlistconfig

This commit is contained in:
Matthias 2023-06-01 20:33:28 +02:00
commit cafc9479b7
19 changed files with 161 additions and 98 deletions

View File

@ -15,7 +15,7 @@ repos:
additional_dependencies:
- types-cachetools==5.3.0.5
- types-filelock==3.2.7
- types-requests==2.30.0.0
- types-requests==2.31.0.0
- types-tabulate==0.9.0.2
- types-python-dateutil==2.8.19.13
- SQLAlchemy==2.0.15
@ -30,7 +30,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.263'
rev: 'v0.0.270'
hooks:
- id: ruff

View File

@ -342,16 +342,12 @@ The above configuration would therefore mean:
The calculation does include fees.
To disable ROI completely, set it to an insanely high number:
To disable ROI completely, set it to an empty dictionary:
```python
minimal_roi = {
"0": 100
}
minimal_roi = {}
```
While technically not completely disabled, this would exit once the trade reaches 10000% Profit.
To use times based on candle duration (timeframe), the following snippet can be handy.
This will allow you to change the timeframe for the strategy, and ROI times will still be set as candles (e.g. after 3 candles ...)

View File

@ -148,7 +148,6 @@ CONF_SCHEMA = {
'patternProperties': {
'^[0-9.]+$': {'type': 'number'}
},
'minProperties': 1
},
'amount_reserve_percent': {'type': 'number', 'minimum': 0.0, 'maximum': 0.5},
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True, 'minimum': -1},

View File

@ -191,7 +191,7 @@ class Exchange:
# Converts the interval provided in minutes in config to seconds
self.markets_refresh_interval: int = exchange_conf.get(
"markets_refresh_interval", 60) * 60
"markets_refresh_interval", 60) * 60 * 1000
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
self.fill_leverage_tiers()
@ -1662,39 +1662,18 @@ class Exchange:
price_side = self._get_price_side(side, is_short, conf_strategy)
price_side_word = price_side.capitalize()
if conf_strategy.get('use_order_book', False):
order_book_top = conf_strategy.get('order_book_top', 1)
if order_book is None:
order_book = self.fetch_l2_order_book(pair, order_book_top)
logger.debug('order_book %s', order_book)
# top 1 = index 0
try:
obside: OBLiteral = 'bids' if price_side == 'bid' else 'asks'
rate = order_book[obside][order_book_top - 1][0]
except (IndexError, KeyError) as e:
logger.warning(
f"{pair} - {name} Price at location {order_book_top} from orderbook "
f"could not be determined. Orderbook: {order_book}"
)
raise PricingError from e
logger.debug(f"{pair} - {name} price from orderbook {price_side_word}"
f"side - top {order_book_top} order book {side} rate {rate:.8f}")
rate = self._get_rate_from_ob(pair, side, order_book, name, price_side,
order_book_top)
else:
logger.debug(f"Using Last {price_side_word} / Last Price")
logger.debug(f"Using Last {price_side.capitalize()} / Last Price")
if ticker is None:
ticker = self.fetch_ticker(pair)
ticker_rate = ticker[price_side]
if ticker['last'] and ticker_rate:
if side == 'entry' and ticker_rate > ticker['last']:
balance = conf_strategy.get('price_last_balance', 0.0)
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
elif side == 'exit' and ticker_rate < ticker['last']:
balance = conf_strategy.get('price_last_balance', 0.0)
ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last'])
rate = ticker_rate
rate = self._get_rate_from_ticker(side, ticker, conf_strategy, price_side)
if rate is None:
raise PricingError(f"{name}-Rate for {pair} was empty.")
@ -1703,6 +1682,43 @@ class Exchange:
return rate
def _get_rate_from_ticker(self, side: EntryExit, ticker: Ticker, conf_strategy: Dict[str, Any],
price_side: BidAsk) -> Optional[float]:
"""
Get rate from ticker.
"""
ticker_rate = ticker[price_side]
if ticker['last'] and ticker_rate:
if side == 'entry' and ticker_rate > ticker['last']:
balance = conf_strategy.get('price_last_balance', 0.0)
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
elif side == 'exit' and ticker_rate < ticker['last']:
balance = conf_strategy.get('price_last_balance', 0.0)
ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last'])
rate = ticker_rate
return rate
def _get_rate_from_ob(self, pair: str, side: EntryExit, order_book: OrderBook, name: str,
price_side: BidAsk, order_book_top: int) -> float:
"""
Get rate from orderbook
:raises: PricingError if rate could not be determined.
"""
logger.debug('order_book %s', order_book)
# top 1 = index 0
try:
obside: OBLiteral = 'bids' if price_side == 'bid' else 'asks'
rate = order_book[obside][order_book_top - 1][0]
except (IndexError, KeyError) as e:
logger.warning(
f"{pair} - {name} Price at location {order_book_top} from orderbook "
f"could not be determined. Orderbook: {order_book}"
)
raise PricingError from e
logger.debug(f"{pair} - {name} price from orderbook {price_side.capitalize()}"
f"side - top {order_book_top} order book {side} rate {rate:.8f}")
return rate
def get_rates(self, pair: str, refresh: bool, is_short: bool) -> Tuple[float, float]:
entry_rate = None
exit_rate = None

View File

@ -33,7 +33,6 @@ class Gate(Exchange):
_ft_has_futures: Dict = {
"needs_trading_fees": True,
"marketOrderRequiresPrice": False,
"tickers_have_bid_ask": False,
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
"stop_price_type_field": "price_type",

View File

@ -1302,6 +1302,10 @@ class FreqtradeBot(LoggingMixin):
f"(orderid:{order['id']}) in order to add another one ...")
self.cancel_stoploss_on_exchange(trade)
if not trade.is_open:
logger.warning(
f"Trade {trade} is closed, not creating trailing stoploss order.")
return
# Create new stoploss order
if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):

View File

@ -32,7 +32,7 @@ def _set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None:
logging.INFO if verbosity <= 2 else logging.DEBUG
)
logging.getLogger('telegram').setLevel(logging.INFO)
logging.getLogger('httpx').setLevel(logging.INFO)
logging.getLogger('httpx').setLevel(logging.WARNING)
logging.getLogger('werkzeug').setLevel(
logging.ERROR if api_verbosity == 'error' else logging.INFO

View File

@ -16,14 +16,14 @@ from freqtrade.exchange.common import remove_exchange_credentials
from freqtrade.misc import deep_merge_dicts
from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest,
BacktestResponse)
from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode
from freqtrade.rpc.api_server.deps import get_config
from freqtrade.rpc.api_server.webserver_bgwork import ApiBG
from freqtrade.rpc.rpc import RPCException
logger = logging.getLogger(__name__)
# Private API, protected by authentication
# Private API, protected by authentication and webserver_mode dependency
router = APIRouter()
@ -102,7 +102,7 @@ def __run_backtest_bg(btconfig: Config):
@router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
async def api_start_backtest(
bt_settings: BacktestRequest, background_tasks: BackgroundTasks,
config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
config=Depends(get_config)):
ApiBG.bt['bt_error'] = None
"""Start backtesting if not done so already"""
if ApiBG.bgtask_running:
@ -143,7 +143,7 @@ async def api_start_backtest(
@router.get('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_get_backtest(ws_mode=Depends(is_webserver_mode)):
def api_get_backtest():
"""
Get backtesting result.
Returns Result after backtesting has been ran.
@ -188,7 +188,7 @@ def api_get_backtest(ws_mode=Depends(is_webserver_mode)):
@router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_delete_backtest(ws_mode=Depends(is_webserver_mode)):
def api_delete_backtest():
"""Reset backtesting"""
if ApiBG.bgtask_running:
return {
@ -215,7 +215,7 @@ def api_delete_backtest(ws_mode=Depends(is_webserver_mode)):
@router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_backtest_abort(ws_mode=Depends(is_webserver_mode)):
def api_backtest_abort():
if not ApiBG.bgtask_running:
return {
"status": "not_running",
@ -236,15 +236,14 @@ def api_backtest_abort(ws_mode=Depends(is_webserver_mode)):
@router.get('/backtest/history', response_model=List[BacktestHistoryEntry],
tags=['webserver', 'backtest'])
def api_backtest_history(config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
def api_backtest_history(config=Depends(get_config)):
# Get backtest result history, read from metadata files
return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results')
@router.get('/backtest/history/result', response_model=BacktestResponse,
tags=['webserver', 'backtest'])
def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config),
ws_mode=Depends(is_webserver_mode)):
def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config)):
# Get backtest result history, read from metadata files
fn = config['user_data_dir'] / 'backtest_results' / filename
results: Dict[str, Any] = {

View File

@ -1,7 +1,7 @@
from typing import Any, AsyncIterator, Dict, Optional
from uuid import uuid4
from fastapi import Depends
from fastapi import Depends, HTTPException
from freqtrade.enums import RunMode
from freqtrade.persistence import Trade
@ -57,5 +57,6 @@ def get_message_stream():
def is_webserver_mode(config=Depends(get_config)):
if config['runmode'] != RunMode.WEBSERVER:
raise RPCException('Bot is not in the correct state')
raise HTTPException(status_code=503,
detail='Bot is not in the correct state.')
return None

View File

@ -118,6 +118,7 @@ class ApiServer(RPCHandler):
from freqtrade.rpc.api_server.api_v1 import router as api_v1
from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public
from freqtrade.rpc.api_server.api_ws import router as ws_router
from freqtrade.rpc.api_server.deps import is_webserver_mode
from freqtrade.rpc.api_server.web_ui import router_ui
app.include_router(api_v1_public, prefix="/api/v1")
@ -126,7 +127,8 @@ class ApiServer(RPCHandler):
dependencies=[Depends(http_basic_or_jwt_token)],
)
app.include_router(api_backtest, prefix="/api/v1",
dependencies=[Depends(http_basic_or_jwt_token)],
dependencies=[Depends(http_basic_or_jwt_token),
Depends(is_webserver_mode)],
)
app.include_router(ws_router, prefix="/api/v1")
app.include_router(router_login, prefix="/api/v1", tags=["auth"])

View File

@ -755,7 +755,7 @@ class RPC:
return {'status': 'Reloaded from orders from exchange'}
def __exec_force_exit(self, trade: Trade, ordertype: Optional[str],
amount: Optional[float] = None) -> None:
amount: Optional[float] = None) -> bool:
# Check if there is there is an open order
fully_canceled = False
if trade.open_order_id:
@ -770,6 +770,9 @@ class RPC:
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_EXIT'])
if not fully_canceled:
if trade.open_order_id is not None:
# Order cancellation failed, so we can't exit.
return False
# Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_rate(
trade.pair, side='exit', is_short=trade.is_short, refresh=True)
@ -790,6 +793,9 @@ class RPC:
trade, current_rate, exit_check, ordertype=order_type,
sub_trade_amt=sub_amount)
return True
return False
def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None, *,
amount: Optional[float] = None) -> Dict[str, str]:
"""
@ -802,12 +808,12 @@ class RPC:
with self._freqtrade._exit_lock:
if trade_id == 'all':
# Execute sell for all open orders
# Execute exit for all open orders
for trade in Trade.get_open_trades():
self.__exec_force_exit(trade, ordertype)
Trade.commit()
self._freqtrade.wallets.update()
return {'result': 'Created sell orders for all open trades.'}
return {'result': 'Created exit orders for all open trades.'}
# Query for trade
trade = Trade.get_trades(
@ -817,10 +823,12 @@ class RPC:
logger.warning('force_exit: Invalid argument received')
raise RPCException('invalid argument')
self.__exec_force_exit(trade, ordertype, amount)
result = self.__exec_force_exit(trade, ordertype, amount)
Trade.commit()
self._freqtrade.wallets.update()
return {'result': f'Created sell order for trade {trade_id}.'}
if not result:
raise RPCException('Failed to exit trade.')
return {'result': f'Created exit order for trade {trade_id}.'}
def _force_entry_validations(self, pair: str, order_side: SignalDirection):
if not self._freqtrade.config.get('force_entry_enable', False):

View File

@ -48,7 +48,7 @@ class IStrategy(ABC, HyperStrategyMixin):
_ft_params_from_file: Dict
# associated minimal roi
minimal_roi: Dict = {"0": 10.0}
minimal_roi: Dict = {}
# associated stoploss
stoploss: float
@ -1265,7 +1265,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:return: minimal ROI entry value or None if none proper ROI entry was found.
"""
# Get highest entry in ROI dict where key <= trade-duration
roi_list = list(filter(lambda x: x <= trade_dur, self.minimal_roi.keys()))
roi_list = [x for x in self.minimal_roi.keys() if x <= trade_dur]
if not roi_list:
return None, None
roi_entry = max(roi_list)

View File

@ -7,12 +7,12 @@
-r docs/requirements-docs.txt
coveralls==3.3.1
ruff==0.0.269
ruff==0.0.270
mypy==1.3.0
pre-commit==3.3.2
pytest==7.3.1
pytest-asyncio==0.21.0
pytest-cov==4.0.0
pytest-cov==4.1.0
pytest-mock==3.10.0
pytest-random-order==1.1.0
isort==5.12.0
@ -25,6 +25,6 @@ nbconvert==7.4.0
# mypy types
types-cachetools==5.3.0.5
types-filelock==3.2.7
types-requests==2.30.0.0
types-requests==2.31.0.0
types-tabulate==0.9.0.2
types-python-dateutil==2.8.19.13

View File

@ -2,16 +2,16 @@ numpy==1.24.3
pandas==2.0.1
pandas-ta==0.3.14b
ccxt==3.1.5
ccxt==3.1.13
cryptography==40.0.2; platform_machine != 'armv7l'
cryptography==40.0.1; platform_machine == 'armv7l'
aiohttp==3.8.4
SQLAlchemy==2.0.15
python-telegram-bot==20.3
# can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.23.3
httpx>=0.24.1
arrow==1.2.3
cachetools==5.3.0
cachetools==5.3.1
requests==2.31.0
urllib3==2.0.2
jsonschema==4.17.3
@ -32,14 +32,14 @@ py_find_1st==1.1.5
# Load ticker files 30% faster
python-rapidjson==1.10
# Properly format api responses
orjson==3.8.12
orjson==3.8.14
# Notify systemd
sdnotify==0.3.2
# API Server
fastapi==0.95.2
pydantic==1.10.7
pydantic==1.10.8
uvicorn==0.22.0
pyjwt==2.7.0
aiofiles==23.1.0

View File

@ -107,7 +107,7 @@ setup(
'ast-comments',
'aiohttp',
'cryptography',
'httpx',
'httpx>=0.24.1',
'python-dateutil',
'packaging',
],

View File

@ -633,21 +633,23 @@ def test__load_markets(default_conf, mocker, caplog):
assert ex.markets == expected_return
def test_reload_markets(default_conf, mocker, caplog):
def test_reload_markets(default_conf, mocker, caplog, time_machine):
caplog.set_level(logging.DEBUG)
initial_markets = {'ETH/BTC': {}}
updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}}
start_dt = dt_now()
time_machine.move_to(start_dt, tick=False)
api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value=initial_markets)
default_conf['exchange']['markets_refresh_interval'] = 10
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance",
mock_markets=False)
exchange._load_async_markets = MagicMock()
exchange._last_markets_refresh = dt_ts()
assert exchange._last_markets_refresh == dt_ts()
assert exchange.markets == initial_markets
time_machine.move_to(start_dt + timedelta(minutes=8), tick=False)
# less than 10 minutes have passed, no reload
exchange.reload_markets()
assert exchange.markets == initial_markets
@ -655,12 +657,18 @@ def test_reload_markets(default_conf, mocker, caplog):
api_mock.load_markets = MagicMock(return_value=updated_markets)
# more than 10 minutes have passed, reload is executed
exchange._last_markets_refresh = dt_ts(dt_now() - timedelta(minutes=15))
time_machine.move_to(start_dt + timedelta(minutes=11), tick=False)
exchange.reload_markets()
assert exchange.markets == updated_markets
assert exchange._load_async_markets.call_count == 1
assert log_has('Performing scheduled market reload..', caplog)
# Not called again
exchange._load_async_markets.reset_mock()
exchange.reload_markets()
assert exchange._load_async_markets.call_count == 0
def test_reload_markets_exception(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG)

View File

@ -820,7 +820,7 @@ tc52 = BTContainer(data=[
[2, 4900, 5250, 4500, 5100, 6172, 0, 0], # Order readjust
[3, 5100, 5100, 4650, 4750, 6172, 0, 0], # stoploss hit?
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.03, roi={"0": 0.10}, profit_perc=-0.03,
stop_loss=-0.03, roi={}, profit_perc=-0.03,
use_exit_signal=True, timeout=1000,
custom_entry_price=4200, adjust_entry_price=5200,
trades=[BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2, is_short=False)]

View File

@ -703,15 +703,15 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
rpc._rpc_force_exit(None)
msg = rpc._rpc_force_exit('all')
assert msg == {'result': 'Created sell orders for all open trades.'}
assert msg == {'result': 'Created exit orders for all open trades.'}
freqtradebot.enter_positions()
msg = rpc._rpc_force_exit('all')
assert msg == {'result': 'Created sell orders for all open trades.'}
assert msg == {'result': 'Created exit orders for all open trades.'}
freqtradebot.enter_positions()
msg = rpc._rpc_force_exit('2')
assert msg == {'result': 'Created sell order for trade 2.'}
assert msg == {'result': 'Created exit order for trade 2.'}
freqtradebot.state = State.STOPPED
with pytest.raises(RPCException, match=r'.*trader is not running*'):
@ -761,27 +761,11 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
freqtradebot.config['max_open_trades'] = 3
freqtradebot.enter_positions()
trade = Trade.session.scalars(select(Trade).filter(Trade.id == '2')).first()
amount = trade.amount
# make an limit-buy open trade, if there is no 'filled', don't sell it
mocker.patch(
f'{EXMS}.fetch_order',
return_value={
'status': 'open',
'type': 'limit',
'side': 'buy',
'filled': None
}
)
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
msg = rpc._rpc_force_exit('4')
assert msg == {'result': 'Created sell order for trade 4.'}
assert cancel_order_mock.call_count == 2
assert trade.amount == amount
cancel_order_mock.reset_mock()
trade = Trade.session.scalars(select(Trade).filter(Trade.id == '3')).first()
# make an limit-sell open trade
amount = trade.amount
# make an limit-sell open order trade
mocker.patch(
f'{EXMS}.fetch_order',
return_value={
@ -794,10 +778,54 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
'id': trade.orders[0].order_id,
}
)
cancel_order_3 = mocker.patch(
f'{EXMS}.cancel_order_with_result',
return_value={
'status': 'canceled',
'type': 'limit',
'side': 'sell',
'amount': amount,
'remaining': amount,
'filled': 0.0,
'id': trade.orders[0].order_id,
}
)
msg = rpc._rpc_force_exit('3')
assert msg == {'result': 'Created sell order for trade 3.'}
assert msg == {'result': 'Created exit order for trade 3.'}
# status quo, no exchange calls
assert cancel_order_mock.call_count == 3
assert cancel_order_3.call_count == 1
assert cancel_order_mock.call_count == 0
trade = Trade.session.scalars(select(Trade).filter(Trade.id == '2')).first()
amount = trade.amount
# make an limit-buy open trade, if there is no 'filled', don't sell it
mocker.patch(
f'{EXMS}.fetch_order',
return_value={
'status': 'open',
'type': 'limit',
'side': 'buy',
'filled': None
}
)
cancel_order_4 = mocker.patch(
f'{EXMS}.cancel_order_with_result',
return_value={
'status': 'canceled',
'type': 'limit',
'side': 'sell',
'amount': amount,
'remaining': 0.0,
'filled': amount,
'id': trade.orders[0].order_id,
}
)
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
msg = rpc._rpc_force_exit('4')
assert msg == {'result': 'Created exit order for trade 4.'}
assert cancel_order_4.call_count == 1
assert cancel_order_mock.call_count == 0
assert trade.amount == amount
def test_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None:

View File

@ -1333,7 +1333,7 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets):
rc = client_post(client, f"{BASE_URI}/forceexit",
data={"tradeid": "5", "ordertype": "market", "amount": 23})
assert_response(rc)
assert rc.json() == {'result': 'Created sell order for trade 5.'}
assert rc.json() == {'result': 'Created exit order for trade 5.'}
Trade.rollback()
trade = Trade.get_trades([Trade.id == 5]).first()
@ -1343,7 +1343,7 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets):
rc = client_post(client, f"{BASE_URI}/forceexit",
data={"tradeid": "5"})
assert_response(rc)
assert rc.json() == {'result': 'Created sell order for trade 5.'}
assert rc.json() == {'result': 'Created exit order for trade 5.'}
Trade.rollback()
trade = Trade.get_trades([Trade.id == 5]).first()
@ -1756,7 +1756,8 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
rc = client_get(client, f"{BASE_URI}/backtest")
# Backtest prevented in default mode
assert_response(rc, 502)
assert_response(rc, 503)
assert rc.json()['detail'] == 'Bot is not in the correct state.'
ftbot.config['runmode'] = RunMode.WEBSERVER
# Backtesting not started yet
@ -1895,7 +1896,9 @@ def test_api_backtest_history(botclient, mocker, testdatadir):
])
rc = client_get(client, f"{BASE_URI}/backtest/history")
assert_response(rc, 502)
assert_response(rc, 503)
assert rc.json()['detail'] == 'Bot is not in the correct state.'
ftbot.config['user_data_dir'] = testdatadir
ftbot.config['runmode'] = RunMode.WEBSERVER