Merge branch 'develop' into feat/pairlistconfig
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 ...)
|
||||||
|
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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] = {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ setup(
|
|||||||
'ast-comments',
|
'ast-comments',
|
||||||
'aiohttp',
|
'aiohttp',
|
||||||
'cryptography',
|
'cryptography',
|
||||||
'httpx',
|
'httpx>=0.24.1',
|
||||||
'python-dateutil',
|
'python-dateutil',
|
||||||
'packaging',
|
'packaging',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user