diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 89f737d71..0c9e8001a 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -14,6 +14,9 @@ Start by downloading and installing Docker / Docker Desktop for your platform: Freqtrade documentation assumes the use of Docker desktop (or the docker compose plugin). While the docker-compose standalone installation still works, it will require changing all `docker compose` commands from `docker compose` to `docker-compose` to work (e.g. `docker compose up -d` will become `docker-compose up -d`). +??? Warning "Docker on windows" + If you just installed docker on a windows system, make sure to reboot your system, otherwise you might encounter unexplainable Problems related to network connectivity to docker containers. + ## Freqtrade with docker Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker compose file](https://github.com/freqtrade/freqtrade/blob/stable/docker-compose.yml) ready for usage. @@ -78,7 +81,7 @@ If you've selected to enable FreqUI in the `new-config` step, you will have freq You can now access the UI by typing localhost:8080 in your browser. -??? Note "UI Access on a remote servers" +??? Note "UI Access on a remote server" If you're running on a VPS, you should consider using either a ssh tunnel, or setup a VPN (openVPN, wireguard) to connect to your bot. This will ensure that freqUI is not directly exposed to the internet, which is not recommended for security reasons (freqUI does not support https out of the box). Setup of these tools is not part of this tutorial, however many good tutorials can be found on the internet. @@ -128,7 +131,7 @@ All freqtrade arguments will be available by running `docker compose run --rm fr !!! Note "`docker compose run --rm`" Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). -??? Note "Using docker without docker" +??? Note "Using docker without docker compose" "`docker compose run --rm`" will require a compose file to be provided. Some freqtrade commands that don't require authentication such as `list-pairs` can be run with "`docker run --rm`" instead. For example `docker run --rm freqtradeorg/freqtrade:stable list-pairs --exchange binance --quote BTC --print-json`. @@ -172,7 +175,7 @@ You can then run `docker compose build --pull` to build the docker image, and ru ### Plotting with docker -Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file. +Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your `docker-compose.yml` file. You can then use these commands as follows: ``` bash @@ -203,16 +206,20 @@ docker compose -f docker/docker-compose-jupyter.yml build --no-cache ### Docker on Windows -* Error: `"Timestamp for this request is outside of the recvWindow."` - * The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past. - To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so). - A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler. +* Error: `"Timestamp for this request is outside of the recvWindow."` + The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past. + To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so). + A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler. - ``` bash - taskkill /IM "Docker Desktop.exe" /F - wsl --shutdown - start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe" - ``` + ``` bash + taskkill /IM "Docker Desktop.exe" /F + wsl --shutdown + start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe" + ``` + +* Cannot connect to the API (Windows) + If you're on windows and just installed Docker (desktop), make sure to reboot your System. Docker can have problems with network connectivity without a restart. + You should obviously also make sure to have your [settings](#accessing-the-ui) accordingly. !!! Warning Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting. diff --git a/docs/faq.md b/docs/faq.md index 7b8cc2580..0a3b152cb 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -20,7 +20,7 @@ Futures trading is supported for selected exchanges. Please refer to the [docume * When you work with your strategy & hyperopt file you should use a proper code editor like VSCode or PyCharm. A good code editor will provide syntax highlighting as well as line numbers, making it easy to find syntax errors (most likely pointed out by Freqtrade during startup). -## Freqtrade common issues +## Freqtrade common questions ### Can freqtrade open multiple positions on the same pair in parallel? @@ -78,6 +78,14 @@ Where possible (e.g. on binance), the use of the exchange's dedicated fee curren On binance, it's sufficient to have BNB in your account, and have "Pay fees in BNB" enabled in your profile. Your BNB balance will slowly decline (as it's used to pay fees) - but you'll no longer encounter dust (Freqtrade will include the fees in the profit calculations). Other exchanges don't offer such possibilities, where it's simply something you'll have to accept or move to a different exchange. +### I deposited more funds to the exchange, but my bot doesn't recognize this + +Freqtrade will update the exchange balance when necessary (Before placing an order). +RPC calls (Telegram's `/balance`, API calls to `/balance`) can trigger an update at max. once per hour. + +If `adjust_trade_position` is enabled (and the bot has open trades eligible for position adjustments) - then the wallets will be refreshed once per hour. +To force an immediate update, you can use `/reload_config` - which will restart the bot. + ### I want to use incomplete candles Freqtrade will not provide incomplete candles to strategies. Using incomplete candles will lead to repainting and consequently to strategies with "ghost" buys, which are impossible to both backtest, and verify after they happened. diff --git a/docs/utils.md b/docs/utils.md index 900856af4..65ab50b9e 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -967,7 +967,7 @@ Print trades with id 2 and 3 as json freqtrade show-trades --db-url sqlite:///tradesv3.sqlite --trade-ids 2 3 --print-json ``` -### Strategy-Updater +## Strategy-Updater Updates listed strategies or all strategies within the strategies folder to be v3 compliant. If the command runs without --strategy-list then all strategies inside the strategies folder will be converted. diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 206588d37..96ab4927e 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -12,7 +12,7 @@ import pandas as pd from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf from freqtrade.exceptions import OperationalException -from freqtrade.misc import json_load +from freqtrade.misc import file_dump_json, json_load from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.types import BacktestHistoryEntryType, BacktestResultType @@ -175,6 +175,21 @@ def _get_backtest_files(dirname: Path) -> List[Path]: return list(reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json')))) +def get_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]: + """ + Get backtest result read from metadata file + """ + return [ + { + 'filename': filename.stem, + 'strategy': s, + 'notes': v.get('notes', ''), + 'run_id': v['run_id'], + 'backtest_start_time': v['backtest_start_time'], + } for s, v in load_backtest_metadata(filename).items() + ] + + def get_backtest_resultlist(dirname: Path) -> List[BacktestHistoryEntryType]: """ Get list of backtest results read from metadata files @@ -184,6 +199,7 @@ def get_backtest_resultlist(dirname: Path) -> List[BacktestHistoryEntryType]: 'filename': filename.stem, 'strategy': s, 'run_id': v['run_id'], + 'notes': v.get('notes', ''), 'backtest_start_time': v['backtest_start_time'], } for filename in _get_backtest_files(dirname) @@ -203,6 +219,21 @@ def delete_backtest_result(file_abs: Path): file_abs_meta.unlink() +def update_backtest_metadata(filename: Path, strategy: str, content: Dict[str, Any]): + """ + Updates backtest metadata file with new content. + :raises: ValueError if metadata file does not exist, or strategy is not in this file. + """ + metadata = load_backtest_metadata(filename) + if not metadata: + raise ValueError("File does not exist.") + if strategy not in metadata: + raise ValueError("Strategy not in metadata.") + metadata[strategy].update(content) + # Write data again. + file_dump_json(get_backtest_metadata_filename(filename), metadata) + + def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str], min_backtest_date: Optional[datetime] = None) -> Dict[str, Any]: """ diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 145a501c9..626643d06 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -7,10 +7,10 @@ import ccxt from freqtrade.constants import BuySell from freqtrade.enums import MarginMode, PriceType, TradingMode +from freqtrade.enums.candletype import CandleType from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier -from freqtrade.exchange.exchange_utils import timeframe_to_msecs logger = logging.getLogger(__name__) @@ -27,7 +27,7 @@ class Bybit(Exchange): """ _ft_has: Dict = { - "ohlcv_candle_limit": 200, + "ohlcv_candle_limit": 1000, "ohlcv_has_history": True, } _ft_has_futures: Dict = { @@ -91,28 +91,13 @@ class Bybit(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e - async def _fetch_funding_rate_history( - self, - pair: str, - timeframe: str, - limit: int, - since_ms: Optional[int] = None, - ) -> List[List]: - """ - Fetch funding rate history - Necessary workaround until https://github.com/ccxt/ccxt/issues/15990 is fixed. - """ - params = {} - if since_ms: - until = since_ms + (timeframe_to_msecs(timeframe) * self._ft_has['ohlcv_candle_limit']) - params.update({'until': until}) - # Funding rate - data = await self._api_async.fetch_funding_rate_history( - pair, since=since_ms, - params=params) - # Convert funding rate to candle pattern - data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data] - return data + def ohlcv_candle_limit( + self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int: + + if candle_type in (CandleType.FUNDING_RATE): + return 200 + + return super().ohlcv_candle_limit(timeframe, candle_type, since_ms) def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False): if self.trading_mode != TradingMode.SPOT: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 279480867..94a159b47 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -610,6 +610,8 @@ class FreqtradeBot(LoggingMixin): # If there is any open orders, wait for them to finish. # TODO Remove to allow mul open orders if trade.open_entry_or_exit_orders_count == 0: + # Do a wallets update (will be ratelimited to once per hour) + self.wallets.update(False) try: self.check_and_call_adjust_trade_position(trade) except DependencyException as exception: diff --git a/freqtrade/optimize/optimize_reports/bt_storage.py b/freqtrade/optimize/optimize_reports/bt_storage.py index 71c6dc130..6b50412b3 100644 --- a/freqtrade/optimize/optimize_reports/bt_storage.py +++ b/freqtrade/optimize/optimize_reports/bt_storage.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) def store_backtest_stats( - recordfilename: Path, stats: BacktestResultType, dtappendix: str) -> None: + recordfilename: Path, stats: BacktestResultType, dtappendix: str) -> Path: """ Stores backtest results :param recordfilename: Path object, which can either be a filename or a directory. @@ -41,6 +41,8 @@ def store_backtest_stats( latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN) file_dump_json(latest_filename, {'latest_backtest': str(filename.name)}) + return filename + def _store_backtest_analysis_data( recordfilename: Path, data: Dict[str, Dict], diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index da1628d8f..5d772febe 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -48,7 +48,7 @@ class Order(ModelBase): id: Mapped[int] = mapped_column(Integer, primary_key=True) ft_trade_id: Mapped[int] = mapped_column(Integer, ForeignKey('trades.id'), index=True) - _trade_live: Mapped["Trade"] = relationship("Trade", back_populates="orders") + _trade_live: Mapped["Trade"] = relationship("Trade", back_populates="orders", lazy="immediate") _trade_bt: "LocalTrade" = None # type: ignore # order_side can only be 'buy', 'sell' or 'stoploss' diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index 6d3174a5a..f387a9ac8 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -10,14 +10,15 @@ from fastapi.exceptions import HTTPException from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.constants import Config -from freqtrade.data.btanalysis import (delete_backtest_result, get_backtest_resultlist, - load_and_merge_backtest_result) +from freqtrade.data.btanalysis import (delete_backtest_result, get_backtest_result, + get_backtest_resultlist, load_and_merge_backtest_result, + update_backtest_metadata) from freqtrade.enums import BacktestState from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange.common import remove_exchange_credentials from freqtrade.misc import deep_merge_dicts, is_file_in_dir -from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest, - BacktestResponse) +from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestMetadataUpdate, + BacktestRequest, BacktestResponse) from freqtrade.rpc.api_server.deps import get_config from freqtrade.rpc.api_server.webserver_bgwork import ApiBG from freqtrade.rpc.rpc import RPCException @@ -74,10 +75,11 @@ def __run_backtest_bg(btconfig: Config): ApiBG.bt['bt'].load_prior_backtest() ApiBG.bt['bt'].abort = False + strategy_name = strat.get_strategy_name() if (ApiBG.bt['bt'].results and - strat.get_strategy_name() in ApiBG.bt['bt'].results['strategy']): + strategy_name in ApiBG.bt['bt'].results['strategy']): # When previous result hash matches - reuse that result and skip backtesting. - logger.info(f'Reusing result of previous backtest for {strat.get_strategy_name()}') + logger.info(f'Reusing result of previous backtest for {strategy_name}') else: min_date, max_date = ApiBG.bt['bt'].backtest_one_strategy( strat, ApiBG.bt['data'], ApiBG.bt['timerange']) @@ -87,10 +89,12 @@ def __run_backtest_bg(btconfig: Config): min_date=min_date, max_date=max_date) if btconfig.get('export', 'none') == 'trades': - store_backtest_stats( + fn = store_backtest_stats( btconfig['exportfilename'], ApiBG.bt['bt'].results, datetime.now().strftime("%Y-%m-%d_%H-%M-%S") ) + ApiBG.bt['bt'].results['metadata'][strategy_name]['filename'] = str(fn.name) + ApiBG.bt['bt'].results['metadata'][strategy_name]['strategy'] = strategy_name logger.info("Backtest finished.") @@ -281,3 +285,24 @@ def api_delete_backtest_history_entry(file: str, config=Depends(get_config)): delete_backtest_result(file_abs) return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results') + + +@router.patch('/backtest/history/{file}', response_model=List[BacktestHistoryEntry], + tags=['webserver', 'backtest']) +def api_update_backtest_history_entry(file: str, body: BacktestMetadataUpdate, + config=Depends(get_config)): + # Get backtest result history, read from metadata files + bt_results_base: Path = config['user_data_dir'] / 'backtest_results' + file_abs = (bt_results_base / file).with_suffix('.json') + # Ensure file is in backtest_results directory + if not is_file_in_dir(file_abs, bt_results_base): + raise HTTPException(status_code=404, detail="File not found.") + content = { + 'notes': body.notes + } + try: + update_backtest_metadata(file_abs, body.strategy, content) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + return get_backtest_result(file_abs) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index bd405d22b..f1ac9db54 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -526,6 +526,12 @@ class BacktestHistoryEntry(BaseModel): strategy: str run_id: str backtest_start_time: int + notes: Optional[str] = '' + + +class BacktestMetadataUpdate(BaseModel): + strategy: str + notes: str = '' class SysInfo(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 3e5d55f71..bc0c88fe4 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -50,7 +50,8 @@ logger = logging.getLogger(__name__) # 2.29: Add /exchanges endpoint # 2.30: new /pairlists endpoint # 2.31: new /backtest/history/ delete endpoint -API_VERSION = 2.31 +# 2.32: new /backtest/history/ patch endpoint +API_VERSION = 2.32 # Public API, requires no auth. router_public = APIRouter() diff --git a/freqtrade/rpc/discord.py b/freqtrade/rpc/discord.py index 8be0eab68..36ef37d01 100644 --- a/freqtrade/rpc/discord.py +++ b/freqtrade/rpc/discord.py @@ -20,6 +20,7 @@ class Discord(Webhook): self._format = 'json' self._retries = 1 self._retry_delay = 0.1 + self._timeout = self._config['discord'].get('timeout', 10) def cleanup(self) -> None: """ diff --git a/freqtrade/types/backtest_result_type.py b/freqtrade/types/backtest_result_type.py index bc53097ab..1043899f7 100644 --- a/freqtrade/types/backtest_result_type.py +++ b/freqtrade/types/backtest_result_type.py @@ -25,3 +25,4 @@ def get_BacktestResultType_default() -> BacktestResultType: class BacktestHistoryEntryType(BacktestMetadataType): filename: str strategy: str + notes: str diff --git a/requirements.txt b/requirements.txt index 7d92f3dea..d0f61d2ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ numpy==1.24.3; python_version <= '3.8' pandas==2.0.3 pandas-ta==0.3.14b -ccxt==4.0.47 +ccxt==4.0.50 cryptography==41.0.3; platform_machine != 'armv7l' cryptography==40.0.1; platform_machine == 'armv7l' aiohttp==3.8.5 @@ -15,7 +15,7 @@ arrow==1.2.3 cachetools==5.3.1 requests==2.31.0 urllib3==2.0.4 -jsonschema==4.18.5 +jsonschema==4.18.6 TA-Lib==0.4.27 technical==1.4.0 tabulate==0.9.0 @@ -33,13 +33,13 @@ py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.10 # Properly format api responses -orjson==3.9.2 +orjson==3.9.3 # Notify systemd sdnotify==0.3.2 # API Server -fastapi==0.100.1 +fastapi==0.101.0 pydantic==1.10.11 uvicorn==0.23.2 pyjwt==2.8.0 diff --git a/tests/exchange/test_bybit.py b/tests/exchange/test_bybit.py index d0d5114a1..7495f543b 100644 --- a/tests/exchange/test_bybit.py +++ b/tests/exchange/test_bybit.py @@ -3,7 +3,6 @@ from unittest.mock import MagicMock from freqtrade.enums.marginmode import MarginMode from freqtrade.enums.tradingmode import TradingMode -from freqtrade.exchange.exchange_utils import timeframe_to_msecs from tests.conftest import get_mock_coro, get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -37,12 +36,10 @@ async def test_bybit_fetch_funding_rate(default_conf, mocker): assert api_mock.fetch_funding_rate_history.call_count == 1 assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT' kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1] - assert kwargs['params'] == {} assert kwargs['since'] is None api_mock.fetch_funding_rate_history.reset_mock() since_ms = 1610000000000 - since_ms_end = since_ms + (timeframe_to_msecs('4h') * limit) # Test fetch_funding_rate_history (current data) await exchange._fetch_funding_rate_history( pair='BTC/USDT:USDT', @@ -54,7 +51,6 @@ async def test_bybit_fetch_funding_rate(default_conf, mocker): assert api_mock.fetch_funding_rate_history.call_count == 1 assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT' kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1] - assert kwargs['params'] == {'until': since_ms_end} assert kwargs['since'] == since_ms diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index c57e32633..2c370145d 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -391,7 +391,7 @@ class TestCCXTExchange: assert po['id'] is not None if len(order.keys()) < 5: # Kucoin case - assert po['status'] == 'closed' + assert po['status'] is None continue assert po['timestamp'] == 1674493798550 assert isinstance(po['datetime'], str) @@ -511,7 +511,8 @@ class TestCCXTExchange: now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2)) assert exch.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now) - def ccxt__async_get_candle_history(self, exchange, exchangename, pair, timeframe, candle_type): + def ccxt__async_get_candle_history( + self, exchange, exchangename, pair, timeframe, candle_type, factor=0.9): timeframe_ms = timeframe_to_msecs(timeframe) now = timeframe_to_prev_date( @@ -532,11 +533,11 @@ class TestCCXTExchange: assert res[1] == timeframe assert res[2] == candle_type candles = res[3] - factor = 0.9 candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * factor candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms * factor assert len(candles) >= min(candle_count, candle_count1), \ f"{len(candles)} < {candle_count} in {timeframe}, Offset: {offset} {factor}" + # Check if first-timeframe is either the start, or start + 1 assert candles[0][0] == since_ms or (since_ms + timeframe_ms) def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE): @@ -544,8 +545,6 @@ class TestCCXTExchange: if exchangename in ('bittrex'): # For some weired reason, this test returns random lengths for bittrex. pytest.skip("Exchange doesn't provide stable ohlcv history") - if exchangename in ('bitvavo'): - pytest.skip("Exchange Downtime ") if not exc._ft_has['ohlcv_has_history']: pytest.skip("Exchange does not support candle history") @@ -554,15 +553,29 @@ class TestCCXTExchange: self.ccxt__async_get_candle_history( exc, exchangename, pair, timeframe, CandleType.SPOT) - def test_ccxt__async_get_candle_history_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): + @pytest.mark.parametrize('candle_type', [ + CandleType.FUTURES, + CandleType.FUNDING_RATE, + CandleType.MARK, + ]) + def test_ccxt__async_get_candle_history_futures( + self, exchange_futures: EXCHANGE_FIXTURE_TYPE, candle_type): exchange, exchangename = exchange_futures if not exchange: # exchange_futures only returns values for supported exchanges return pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair']) timeframe = EXCHANGES[exchangename]['timeframe'] + if candle_type == CandleType.FUNDING_RATE: + timeframe = exchange._ft_has.get('funding_fee_timeframe', + exchange._ft_has['mark_ohlcv_timeframe']) self.ccxt__async_get_candle_history( - exchange, exchangename, pair, timeframe, CandleType.FUTURES) + exchange, + exchangename, + pair=pair, + timeframe=timeframe, + candle_type=candle_type, + ) def test_ccxt_fetch_funding_rate_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): exchange, exchangename = exchange_futures diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 289a67c4a..a8c765553 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2100,7 +2100,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_ exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) # one_call calculation * 1.8 should do 2 calls - since = 5 * 60 * exchange.ohlcv_candle_limit('5m', CandleType.SPOT) * 1.8 + since = 5 * 60 * exchange.ohlcv_candle_limit('5m', candle_type) * 1.8 ret = exchange.get_historic_ohlcv( pair, "5m", diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 1af79d747..d89bbbff9 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -10,6 +10,7 @@ from unittest.mock import ANY, MagicMock, PropertyMock import pandas as pd import pytest +import rapidjson import uvicorn from fastapi import FastAPI, WebSocketDisconnect from fastapi.exceptions import HTTPException @@ -80,6 +81,16 @@ def client_post(client: TestClient, url, data={}): }) +def client_patch(client: TestClient, url, data={}): + + return client.patch(url, + json=data, + headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS), + 'Origin': 'http://example.com', + 'content-type': 'application/json' + }) + + def client_get(client: TestClient, url): # Add fake Origin to ensure CORS kicks in return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS), @@ -1758,7 +1769,7 @@ def test_api_pairlists_evaluate(botclient, tmpdir, mocker): rc = client_get(client, f"{BASE_URI}/pairlists/evaluate/{job_id}") assert_response(rc) response = rc.json() - assert response['result']['whitelist'] == ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC',] + assert response['result']['whitelist'] == ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'] assert response['result']['length'] == 4 # Restart with additional filter, reducing the list to 2 @@ -2005,6 +2016,7 @@ def test_api_backtest_history(botclient, mocker, testdatadir): assert len(result) == 3 fn = result[0]['filename'] assert fn == "backtest-result_multistrat" + assert result[0]['notes'] == '' strategy = result[0]['strategy'] rc = client_get(client, f"{BASE_URI}/backtest/history/result?filename={fn}&strategy={strategy}") assert_response(rc) @@ -2018,7 +2030,7 @@ def test_api_backtest_history(botclient, mocker, testdatadir): assert result2['backtest_result']['strategy'][strategy] -def test_api_delete_backtest_history_entry(botclient, mocker, tmp_path: Path): +def test_api_delete_backtest_history_entry(botclient, tmp_path: Path): ftbot, client = botclient # Create a temporary directory and file @@ -2046,6 +2058,75 @@ def test_api_delete_backtest_history_entry(botclient, mocker, tmp_path: Path): assert not meta_path.exists() +def test_api_patch_backtest_history_entry(botclient, tmp_path: Path): + ftbot, client = botclient + + # Create a temporary directory and file + bt_results_base = tmp_path / "backtest_results" + bt_results_base.mkdir() + file_path = bt_results_base / "test.json" + file_path.touch() + meta_path = file_path.with_suffix('.meta.json') + with meta_path.open('w') as metafile: + rapidjson.dump({ + CURRENT_TEST_STRATEGY: { + "run_id": "6e542efc8d5e62cef6e5be0ffbc29be81a6e751d", + "backtest_start_time": 1690176003} + }, metafile) + + def read_metadata(): + with meta_path.open('r') as metafile: + return rapidjson.load(metafile) + + rc = client_patch(client, f"{BASE_URI}/backtest/history/randomFile.json") + assert_response(rc, 503) + + ftbot.config['user_data_dir'] = tmp_path + ftbot.config['runmode'] = RunMode.WEBSERVER + + rc = client_patch(client, f"{BASE_URI}/backtest/history/randomFile.json", { + "strategy": CURRENT_TEST_STRATEGY, + }) + assert rc.status_code == 404 + + # Nonexisting strategy + rc = client_patch(client, f"{BASE_URI}/backtest/history/{file_path.name}", { + "strategy": f"{CURRENT_TEST_STRATEGY}xxx", + }) + assert rc.status_code == 400 + assert rc.json()['detail'] == 'Strategy not in metadata.' + + # no Notes + rc = client_patch(client, f"{BASE_URI}/backtest/history/{file_path.name}", { + "strategy": CURRENT_TEST_STRATEGY, + }) + assert rc.status_code == 200 + res = rc.json() + assert isinstance(res, list) + assert len(res) == 1 + assert res[0]['strategy'] == CURRENT_TEST_STRATEGY + assert res[0]['notes'] == '' + + fileres = read_metadata() + assert fileres[CURRENT_TEST_STRATEGY]['run_id'] == res[0]['run_id'] + assert fileres[CURRENT_TEST_STRATEGY]['notes'] == '' + + rc = client_patch(client, f"{BASE_URI}/backtest/history/{file_path.name}", { + "strategy": CURRENT_TEST_STRATEGY, + "notes": "FooBar", + }) + assert rc.status_code == 200 + res = rc.json() + assert isinstance(res, list) + assert len(res) == 1 + assert res[0]['strategy'] == CURRENT_TEST_STRATEGY + assert res[0]['notes'] == 'FooBar' + + fileres = read_metadata() + assert fileres[CURRENT_TEST_STRATEGY]['run_id'] == res[0]['run_id'] + assert fileres[CURRENT_TEST_STRATEGY]['notes'] == 'FooBar' + + def test_health(botclient): ftbot, client = botclient diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index 2d5121403..571427fb1 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -197,7 +197,7 @@ class StrategyTestV3(IStrategy): if current_profit < -0.0075: orders = trade.select_filled_orders(trade.entry_side) - return round(orders[0].safe_cost, 0) + return round(orders[0].stake_amount, 0) return None diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ca89c7666..dcba0dc19 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1501,9 +1501,9 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, @pytest.mark.parametrize("is_short", [False, True]) def test_create_stoploss_order_invalid_order( - mocker, default_conf_usdt, caplog, fee, is_short, limit_order, limit_order_open + mocker, default_conf_usdt, caplog, fee, is_short, limit_order ): - open_order = limit_order_open[entry_side(is_short)] + open_order = limit_order[entry_side(is_short)] order = limit_order[exit_side(is_short)] rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) @@ -1534,6 +1534,7 @@ def test_create_stoploss_order_invalid_order( trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short caplog.clear() + rpc_mock.reset_mock() freqtrade.create_stoploss_order(trade, 200) assert trade.stoploss_order_id is None assert trade.exit_reason == ExitType.EMERGENCY_EXIT.value @@ -1547,9 +1548,11 @@ def test_create_stoploss_order_invalid_order( assert create_order_mock.call_args[1]['amount'] == trade.amount # Rpc is sending first buy, then sell - assert rpc_mock.call_count == 3 - assert rpc_mock.call_args_list[2][0][0]['sell_reason'] == ExitType.EMERGENCY_EXIT.value - assert rpc_mock.call_args_list[2][0][0]['order_type'] == 'market' + assert rpc_mock.call_count == 2 + assert rpc_mock.call_args_list[0][0][0]['sell_reason'] == ExitType.EMERGENCY_EXIT.value + assert rpc_mock.call_args_list[0][0][0]['order_type'] == 'market' + assert rpc_mock.call_args_list[0][0][0]['type'] == 'exit' + assert rpc_mock.call_args_list[1][0][0]['type'] == 'exit_fill' @pytest.mark.parametrize("is_short", [False, True])