mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge branch 'develop' into feat/pairlistconfig
This commit is contained in:
commit
cafc9479b7
|
@ -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
|
||||
|
||||
|
|
|
@ -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 ...)
|
||||
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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] = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
2
setup.py
2
setup.py
|
@ -107,7 +107,7 @@ setup(
|
|||
'ast-comments',
|
||||
'aiohttp',
|
||||
'cryptography',
|
||||
'httpx',
|
||||
'httpx>=0.24.1',
|
||||
'python-dateutil',
|
||||
'packaging',
|
||||
],
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user