Merge branch 'develop' into pr/Axel-CH/8779

This commit is contained in:
Matthias 2023-08-09 19:58:24 +02:00
commit 88925d6c1d
20 changed files with 235 additions and 73 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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]:
"""

View File

@ -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:

View File

@ -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:

View File

@ -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],

View File

@ -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'

View File

@ -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)

View File

@ -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):

View File

@ -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()

View File

@ -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:
"""

View File

@ -25,3 +25,4 @@ def get_BacktestResultType_default() -> BacktestResultType:
class BacktestHistoryEntryType(BacktestMetadataType):
filename: str
strategy: str
notes: str

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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])