Merge branch 'develop' into feat/pairlistconfig

This commit is contained in:
Matthias
2023-06-01 20:33:28 +02:00
19 changed files with 161 additions and 98 deletions
+2 -2
View File
@@ -15,7 +15,7 @@ repos:
additional_dependencies: additional_dependencies:
- types-cachetools==5.3.0.5 - types-cachetools==5.3.0.5
- types-filelock==3.2.7 - types-filelock==3.2.7
- types-requests==2.30.0.0 - types-requests==2.31.0.0
- types-tabulate==0.9.0.2 - types-tabulate==0.9.0.2
- types-python-dateutil==2.8.19.13 - types-python-dateutil==2.8.19.13
- SQLAlchemy==2.0.15 - SQLAlchemy==2.0.15
@@ -30,7 +30,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: 'v0.0.263' rev: 'v0.0.270'
hooks: hooks:
- id: ruff - id: ruff
+2 -6
View File
@@ -342,16 +342,12 @@ The above configuration would therefore mean:
The calculation does include fees. 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 ```python
minimal_roi = { minimal_roi = {}
"0": 100
}
``` ```
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. 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 ...) 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 ...)
-1
View File
@@ -148,7 +148,6 @@ CONF_SCHEMA = {
'patternProperties': { 'patternProperties': {
'^[0-9.]+$': {'type': 'number'} '^[0-9.]+$': {'type': 'number'}
}, },
'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, 'minimum': -1}, 'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True, 'minimum': -1},
+42 -26
View File
@@ -191,7 +191,7 @@ class Exchange:
# Converts the interval provided in minutes in config to seconds # Converts the interval provided in minutes in config to seconds
self.markets_refresh_interval: int = exchange_conf.get( 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: if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
self.fill_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 = self._get_price_side(side, is_short, conf_strategy)
price_side_word = price_side.capitalize()
if conf_strategy.get('use_order_book', False): if conf_strategy.get('use_order_book', False):
order_book_top = conf_strategy.get('order_book_top', 1) order_book_top = conf_strategy.get('order_book_top', 1)
if order_book is None: if order_book is None:
order_book = self.fetch_l2_order_book(pair, order_book_top) order_book = self.fetch_l2_order_book(pair, order_book_top)
logger.debug('order_book %s', order_book) rate = self._get_rate_from_ob(pair, side, order_book, name, price_side,
# top 1 = index 0 order_book_top)
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}")
else: 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: if ticker is None:
ticker = self.fetch_ticker(pair) ticker = self.fetch_ticker(pair)
ticker_rate = ticker[price_side] rate = self._get_rate_from_ticker(side, ticker, conf_strategy, 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
if rate is None: if rate is None:
raise PricingError(f"{name}-Rate for {pair} was empty.") raise PricingError(f"{name}-Rate for {pair} was empty.")
@@ -1703,6 +1682,43 @@ class Exchange:
return rate 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]: def get_rates(self, pair: str, refresh: bool, is_short: bool) -> Tuple[float, float]:
entry_rate = None entry_rate = None
exit_rate = None exit_rate = None
-1
View File
@@ -33,7 +33,6 @@ class Gate(Exchange):
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
"needs_trading_fees": True, "needs_trading_fees": True,
"marketOrderRequiresPrice": False, "marketOrderRequiresPrice": False,
"tickers_have_bid_ask": False,
"fee_cost_in_contracts": False, # Set explicitly to false for clarity "fee_cost_in_contracts": False, # Set explicitly to false for clarity
"order_props_in_contracts": ['amount', 'filled', 'remaining'], "order_props_in_contracts": ['amount', 'filled', 'remaining'],
"stop_price_type_field": "price_type", "stop_price_type_field": "price_type",
+4
View File
@@ -1302,6 +1302,10 @@ class FreqtradeBot(LoggingMixin):
f"(orderid:{order['id']}) in order to add another one ...") f"(orderid:{order['id']}) in order to add another one ...")
self.cancel_stoploss_on_exchange(trade) 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 # Create new stoploss order
if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm): if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):
+1 -1
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.INFO if verbosity <= 2 else logging.DEBUG
) )
logging.getLogger('telegram').setLevel(logging.INFO) logging.getLogger('telegram').setLevel(logging.INFO)
logging.getLogger('httpx').setLevel(logging.INFO) logging.getLogger('httpx').setLevel(logging.WARNING)
logging.getLogger('werkzeug').setLevel( logging.getLogger('werkzeug').setLevel(
logging.ERROR if api_verbosity == 'error' else logging.INFO logging.ERROR if api_verbosity == 'error' else logging.INFO
+8 -9
View File
@@ -16,14 +16,14 @@ from freqtrade.exchange.common import remove_exchange_credentials
from freqtrade.misc import deep_merge_dicts from freqtrade.misc import deep_merge_dicts
from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest, from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest,
BacktestResponse) 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.api_server.webserver_bgwork import ApiBG
from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.rpc import RPCException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Private API, protected by authentication # Private API, protected by authentication and webserver_mode dependency
router = APIRouter() router = APIRouter()
@@ -102,7 +102,7 @@ def __run_backtest_bg(btconfig: Config):
@router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) @router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
async def api_start_backtest( async def api_start_backtest(
bt_settings: BacktestRequest, background_tasks: BackgroundTasks, 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 ApiBG.bt['bt_error'] = None
"""Start backtesting if not done so already""" """Start backtesting if not done so already"""
if ApiBG.bgtask_running: if ApiBG.bgtask_running:
@@ -143,7 +143,7 @@ async def api_start_backtest(
@router.get('/backtest', response_model=BacktestResponse, tags=['webserver', '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. Get backtesting result.
Returns Result after backtesting has been ran. 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']) @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""" """Reset backtesting"""
if ApiBG.bgtask_running: if ApiBG.bgtask_running:
return { 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']) @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: if not ApiBG.bgtask_running:
return { return {
"status": "not_running", "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], @router.get('/backtest/history', response_model=List[BacktestHistoryEntry],
tags=['webserver', 'backtest']) 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 # Get backtest result history, read from metadata files
return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results') return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results')
@router.get('/backtest/history/result', response_model=BacktestResponse, @router.get('/backtest/history/result', response_model=BacktestResponse,
tags=['webserver', 'backtest']) tags=['webserver', 'backtest'])
def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config), def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config)):
ws_mode=Depends(is_webserver_mode)):
# Get backtest result history, read from metadata files # Get backtest result history, read from metadata files
fn = config['user_data_dir'] / 'backtest_results' / filename fn = config['user_data_dir'] / 'backtest_results' / filename
results: Dict[str, Any] = { results: Dict[str, Any] = {
+3 -2
View File
@@ -1,7 +1,7 @@
from typing import Any, AsyncIterator, Dict, Optional from typing import Any, AsyncIterator, Dict, Optional
from uuid import uuid4 from uuid import uuid4
from fastapi import Depends from fastapi import Depends, HTTPException
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@@ -57,5 +57,6 @@ def get_message_stream():
def is_webserver_mode(config=Depends(get_config)): def is_webserver_mode(config=Depends(get_config)):
if config['runmode'] != RunMode.WEBSERVER: 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 return None
+3 -1
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 as api_v1
from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public 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.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 from freqtrade.rpc.api_server.web_ui import router_ui
app.include_router(api_v1_public, prefix="/api/v1") app.include_router(api_v1_public, prefix="/api/v1")
@@ -126,7 +127,8 @@ class ApiServer(RPCHandler):
dependencies=[Depends(http_basic_or_jwt_token)], dependencies=[Depends(http_basic_or_jwt_token)],
) )
app.include_router(api_backtest, prefix="/api/v1", 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(ws_router, prefix="/api/v1")
app.include_router(router_login, prefix="/api/v1", tags=["auth"]) app.include_router(router_login, prefix="/api/v1", tags=["auth"])
+13 -5
View File
@@ -755,7 +755,7 @@ class RPC:
return {'status': 'Reloaded from orders from exchange'} return {'status': 'Reloaded from orders from exchange'}
def __exec_force_exit(self, trade: Trade, ordertype: Optional[str], 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 # Check if there is there is an open order
fully_canceled = False fully_canceled = False
if trade.open_order_id: if trade.open_order_id:
@@ -770,6 +770,9 @@ class RPC:
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_EXIT']) self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_EXIT'])
if not fully_canceled: 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 # Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_rate( current_rate = self._freqtrade.exchange.get_rate(
trade.pair, side='exit', is_short=trade.is_short, refresh=True) 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, trade, current_rate, exit_check, ordertype=order_type,
sub_trade_amt=sub_amount) sub_trade_amt=sub_amount)
return True
return False
def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None, *, def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None, *,
amount: Optional[float] = None) -> Dict[str, str]: amount: Optional[float] = None) -> Dict[str, str]:
""" """
@@ -802,12 +808,12 @@ class RPC:
with self._freqtrade._exit_lock: with self._freqtrade._exit_lock:
if trade_id == 'all': if trade_id == 'all':
# Execute sell for all open orders # Execute exit for all open orders
for trade in Trade.get_open_trades(): for trade in Trade.get_open_trades():
self.__exec_force_exit(trade, ordertype) self.__exec_force_exit(trade, ordertype)
Trade.commit() Trade.commit()
self._freqtrade.wallets.update() 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 # Query for trade
trade = Trade.get_trades( trade = Trade.get_trades(
@@ -817,10 +823,12 @@ class RPC:
logger.warning('force_exit: Invalid argument received') logger.warning('force_exit: Invalid argument received')
raise RPCException('invalid argument') raise RPCException('invalid argument')
self.__exec_force_exit(trade, ordertype, amount) result = self.__exec_force_exit(trade, ordertype, amount)
Trade.commit() Trade.commit()
self._freqtrade.wallets.update() 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): def _force_entry_validations(self, pair: str, order_side: SignalDirection):
if not self._freqtrade.config.get('force_entry_enable', False): if not self._freqtrade.config.get('force_entry_enable', False):
+2 -2
View File
@@ -48,7 +48,7 @@ class IStrategy(ABC, HyperStrategyMixin):
_ft_params_from_file: Dict _ft_params_from_file: Dict
# associated minimal roi # associated minimal roi
minimal_roi: Dict = {"0": 10.0} minimal_roi: Dict = {}
# associated stoploss # associated stoploss
stoploss: float stoploss: float
@@ -1265,7 +1265,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:return: minimal ROI entry value or None if none proper ROI entry was found. :return: minimal ROI entry value or None if none proper ROI entry was found.
""" """
# Get highest entry in ROI dict where key <= trade-duration # 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: if not roi_list:
return None, None return None, None
roi_entry = max(roi_list) roi_entry = max(roi_list)
+3 -3
View File
@@ -7,12 +7,12 @@
-r docs/requirements-docs.txt -r docs/requirements-docs.txt
coveralls==3.3.1 coveralls==3.3.1
ruff==0.0.269 ruff==0.0.270
mypy==1.3.0 mypy==1.3.0
pre-commit==3.3.2 pre-commit==3.3.2
pytest==7.3.1 pytest==7.3.1
pytest-asyncio==0.21.0 pytest-asyncio==0.21.0
pytest-cov==4.0.0 pytest-cov==4.1.0
pytest-mock==3.10.0 pytest-mock==3.10.0
pytest-random-order==1.1.0 pytest-random-order==1.1.0
isort==5.12.0 isort==5.12.0
@@ -25,6 +25,6 @@ nbconvert==7.4.0
# mypy types # mypy types
types-cachetools==5.3.0.5 types-cachetools==5.3.0.5
types-filelock==3.2.7 types-filelock==3.2.7
types-requests==2.30.0.0 types-requests==2.31.0.0
types-tabulate==0.9.0.2 types-tabulate==0.9.0.2
types-python-dateutil==2.8.19.13 types-python-dateutil==2.8.19.13
+5 -5
View File
@@ -2,16 +2,16 @@ numpy==1.24.3
pandas==2.0.1 pandas==2.0.1
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==3.1.5 ccxt==3.1.13
cryptography==40.0.2; platform_machine != 'armv7l' cryptography==40.0.2; platform_machine != 'armv7l'
cryptography==40.0.1; platform_machine == 'armv7l' cryptography==40.0.1; platform_machine == 'armv7l'
aiohttp==3.8.4 aiohttp==3.8.4
SQLAlchemy==2.0.15 SQLAlchemy==2.0.15
python-telegram-bot==20.3 python-telegram-bot==20.3
# can't be hard-pinned due to telegram-bot pinning httpx with ~ # can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.23.3 httpx>=0.24.1
arrow==1.2.3 arrow==1.2.3
cachetools==5.3.0 cachetools==5.3.1
requests==2.31.0 requests==2.31.0
urllib3==2.0.2 urllib3==2.0.2
jsonschema==4.17.3 jsonschema==4.17.3
@@ -32,14 +32,14 @@ py_find_1st==1.1.5
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.10 python-rapidjson==1.10
# Properly format api responses # Properly format api responses
orjson==3.8.12 orjson==3.8.14
# Notify systemd # Notify systemd
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.95.2 fastapi==0.95.2
pydantic==1.10.7 pydantic==1.10.8
uvicorn==0.22.0 uvicorn==0.22.0
pyjwt==2.7.0 pyjwt==2.7.0
aiofiles==23.1.0 aiofiles==23.1.0
+1 -1
View File
@@ -107,7 +107,7 @@ setup(
'ast-comments', 'ast-comments',
'aiohttp', 'aiohttp',
'cryptography', 'cryptography',
'httpx', 'httpx>=0.24.1',
'python-dateutil', 'python-dateutil',
'packaging', 'packaging',
], ],
+12 -4
View File
@@ -633,21 +633,23 @@ def test__load_markets(default_conf, mocker, caplog):
assert ex.markets == expected_return 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) caplog.set_level(logging.DEBUG)
initial_markets = {'ETH/BTC': {}} initial_markets = {'ETH/BTC': {}}
updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}} updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}}
start_dt = dt_now()
time_machine.move_to(start_dt, tick=False)
api_mock = MagicMock() api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value=initial_markets) api_mock.load_markets = MagicMock(return_value=initial_markets)
default_conf['exchange']['markets_refresh_interval'] = 10 default_conf['exchange']['markets_refresh_interval'] = 10
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance", exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance",
mock_markets=False) mock_markets=False)
exchange._load_async_markets = MagicMock() exchange._load_async_markets = MagicMock()
exchange._last_markets_refresh = dt_ts() assert exchange._last_markets_refresh == dt_ts()
assert exchange.markets == initial_markets assert exchange.markets == initial_markets
time_machine.move_to(start_dt + timedelta(minutes=8), tick=False)
# less than 10 minutes have passed, no reload # less than 10 minutes have passed, no reload
exchange.reload_markets() exchange.reload_markets()
assert exchange.markets == initial_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) api_mock.load_markets = MagicMock(return_value=updated_markets)
# more than 10 minutes have passed, reload is executed # 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() exchange.reload_markets()
assert exchange.markets == updated_markets assert exchange.markets == updated_markets
assert exchange._load_async_markets.call_count == 1 assert exchange._load_async_markets.call_count == 1
assert log_has('Performing scheduled market reload..', caplog) 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): def test_reload_markets_exception(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
+1 -1
View File
@@ -820,7 +820,7 @@ tc52 = BTContainer(data=[
[2, 4900, 5250, 4500, 5100, 6172, 0, 0], # Order readjust [2, 4900, 5250, 4500, 5100, 6172, 0, 0], # Order readjust
[3, 5100, 5100, 4650, 4750, 6172, 0, 0], # stoploss hit? [3, 5100, 5100, 4650, 4750, 6172, 0, 0], # stoploss hit?
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]], [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, use_exit_signal=True, timeout=1000,
custom_entry_price=4200, adjust_entry_price=5200, custom_entry_price=4200, adjust_entry_price=5200,
trades=[BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2, is_short=False)] trades=[BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2, is_short=False)]
+52 -24
View File
@@ -703,15 +703,15 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
rpc._rpc_force_exit(None) rpc._rpc_force_exit(None)
msg = rpc._rpc_force_exit('all') 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() freqtradebot.enter_positions()
msg = rpc._rpc_force_exit('all') 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() freqtradebot.enter_positions()
msg = rpc._rpc_force_exit('2') 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 freqtradebot.state = State.STOPPED
with pytest.raises(RPCException, match=r'.*trader is not running*'): 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.config['max_open_trades'] = 3
freqtradebot.enter_positions() 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() trade = Trade.session.scalars(select(Trade).filter(Trade.id == '3')).first()
amount = trade.amount
# make an limit-sell open trade # make an limit-sell open order trade
mocker.patch( mocker.patch(
f'{EXMS}.fetch_order', f'{EXMS}.fetch_order',
return_value={ return_value={
@@ -794,10 +778,54 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
'id': trade.orders[0].order_id, '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') 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 # 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: def test_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None:
+7 -4
View File
@@ -1333,7 +1333,7 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets):
rc = client_post(client, f"{BASE_URI}/forceexit", rc = client_post(client, f"{BASE_URI}/forceexit",
data={"tradeid": "5", "ordertype": "market", "amount": 23}) data={"tradeid": "5", "ordertype": "market", "amount": 23})
assert_response(rc) 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.rollback()
trade = Trade.get_trades([Trade.id == 5]).first() 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", rc = client_post(client, f"{BASE_URI}/forceexit",
data={"tradeid": "5"}) data={"tradeid": "5"})
assert_response(rc) 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.rollback()
trade = Trade.get_trades([Trade.id == 5]).first() 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") rc = client_get(client, f"{BASE_URI}/backtest")
# Backtest prevented in default mode # 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 ftbot.config['runmode'] = RunMode.WEBSERVER
# Backtesting not started yet # 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") 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['user_data_dir'] = testdatadir
ftbot.config['runmode'] = RunMode.WEBSERVER ftbot.config['runmode'] = RunMode.WEBSERVER