mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 08:33:07 +00:00
Merge branch 'develop' into pr/Axel-CH/8779
This commit is contained in:
@@ -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).
|
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`).
|
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 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.
|
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.
|
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.
|
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).
|
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.
|
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`"
|
!!! 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).
|
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.
|
"`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.
|
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`.
|
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
|
### 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:
|
You can then use these commands as follows:
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
@@ -203,16 +206,20 @@ docker compose -f docker/docker-compose-jupyter.yml build --no-cache
|
|||||||
|
|
||||||
### Docker on Windows
|
### Docker on Windows
|
||||||
|
|
||||||
* Error: `"Timestamp for this request is outside of the recvWindow."`
|
* 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.
|
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).
|
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.
|
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
|
``` bash
|
||||||
taskkill /IM "Docker Desktop.exe" /F
|
taskkill /IM "Docker Desktop.exe" /F
|
||||||
wsl --shutdown
|
wsl --shutdown
|
||||||
start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
|
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
|
!!! 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.
|
Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting.
|
||||||
|
|||||||
10
docs/faq.md
10
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).
|
* 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?
|
### 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).
|
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.
|
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
|
### 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.
|
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.
|
||||||
|
|||||||
@@ -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
|
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.
|
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.
|
If the command runs without --strategy-list then all strategies inside the strategies folder will be converted.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import pandas as pd
|
|||||||
|
|
||||||
from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf
|
from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf
|
||||||
from freqtrade.exceptions import OperationalException
|
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.optimize.backtest_caching import get_backtest_metadata_filename
|
||||||
from freqtrade.persistence import LocalTrade, Trade, init_db
|
from freqtrade.persistence import LocalTrade, Trade, init_db
|
||||||
from freqtrade.types import BacktestHistoryEntryType, BacktestResultType
|
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'))))
|
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]:
|
def get_backtest_resultlist(dirname: Path) -> List[BacktestHistoryEntryType]:
|
||||||
"""
|
"""
|
||||||
Get list of backtest results read from metadata files
|
Get list of backtest results read from metadata files
|
||||||
@@ -184,6 +199,7 @@ def get_backtest_resultlist(dirname: Path) -> List[BacktestHistoryEntryType]:
|
|||||||
'filename': filename.stem,
|
'filename': filename.stem,
|
||||||
'strategy': s,
|
'strategy': s,
|
||||||
'run_id': v['run_id'],
|
'run_id': v['run_id'],
|
||||||
|
'notes': v.get('notes', ''),
|
||||||
'backtest_start_time': v['backtest_start_time'],
|
'backtest_start_time': v['backtest_start_time'],
|
||||||
}
|
}
|
||||||
for filename in _get_backtest_files(dirname)
|
for filename in _get_backtest_files(dirname)
|
||||||
@@ -203,6 +219,21 @@ def delete_backtest_result(file_abs: Path):
|
|||||||
file_abs_meta.unlink()
|
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],
|
def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str],
|
||||||
min_backtest_date: Optional[datetime] = None) -> Dict[str, Any]:
|
min_backtest_date: Optional[datetime] = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import ccxt
|
|||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
||||||
|
from freqtrade.enums.candletype import CandleType
|
||||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
from freqtrade.exchange.exchange_utils import timeframe_to_msecs
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -27,7 +27,7 @@ class Bybit(Exchange):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"ohlcv_candle_limit": 200,
|
"ohlcv_candle_limit": 1000,
|
||||||
"ohlcv_has_history": True,
|
"ohlcv_has_history": True,
|
||||||
}
|
}
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
@@ -91,28 +91,13 @@ class Bybit(Exchange):
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
async def _fetch_funding_rate_history(
|
def ohlcv_candle_limit(
|
||||||
self,
|
self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
|
||||||
pair: str,
|
|
||||||
timeframe: str,
|
if candle_type in (CandleType.FUNDING_RATE):
|
||||||
limit: int,
|
return 200
|
||||||
since_ms: Optional[int] = None,
|
|
||||||
) -> List[List]:
|
return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
|
||||||
"""
|
|
||||||
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 _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
||||||
if self.trading_mode != TradingMode.SPOT:
|
if self.trading_mode != TradingMode.SPOT:
|
||||||
|
|||||||
@@ -610,6 +610,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# If there is any open orders, wait for them to finish.
|
# If there is any open orders, wait for them to finish.
|
||||||
# TODO Remove to allow mul open orders
|
# TODO Remove to allow mul open orders
|
||||||
if trade.open_entry_or_exit_orders_count == 0:
|
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:
|
try:
|
||||||
self.check_and_call_adjust_trade_position(trade)
|
self.check_and_call_adjust_trade_position(trade)
|
||||||
except DependencyException as exception:
|
except DependencyException as exception:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def store_backtest_stats(
|
def store_backtest_stats(
|
||||||
recordfilename: Path, stats: BacktestResultType, dtappendix: str) -> None:
|
recordfilename: Path, stats: BacktestResultType, dtappendix: str) -> Path:
|
||||||
"""
|
"""
|
||||||
Stores backtest results
|
Stores backtest results
|
||||||
:param recordfilename: Path object, which can either be a filename or a directory.
|
: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)
|
latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN)
|
||||||
file_dump_json(latest_filename, {'latest_backtest': str(filename.name)})
|
file_dump_json(latest_filename, {'latest_backtest': str(filename.name)})
|
||||||
|
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
def _store_backtest_analysis_data(
|
def _store_backtest_analysis_data(
|
||||||
recordfilename: Path, data: Dict[str, Dict],
|
recordfilename: Path, data: Dict[str, Dict],
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class Order(ModelBase):
|
|||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
ft_trade_id: Mapped[int] = mapped_column(Integer, ForeignKey('trades.id'), index=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
|
_trade_bt: "LocalTrade" = None # type: ignore
|
||||||
|
|
||||||
# order_side can only be 'buy', 'sell' or 'stoploss'
|
# order_side can only be 'buy', 'sell' or 'stoploss'
|
||||||
|
|||||||
@@ -10,14 +10,15 @@ from fastapi.exceptions import HTTPException
|
|||||||
|
|
||||||
from freqtrade.configuration.config_validation import validate_config_consistency
|
from freqtrade.configuration.config_validation import validate_config_consistency
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.data.btanalysis import (delete_backtest_result, get_backtest_resultlist,
|
from freqtrade.data.btanalysis import (delete_backtest_result, get_backtest_result,
|
||||||
load_and_merge_backtest_result)
|
get_backtest_resultlist, load_and_merge_backtest_result,
|
||||||
|
update_backtest_metadata)
|
||||||
from freqtrade.enums import BacktestState
|
from freqtrade.enums import BacktestState
|
||||||
from freqtrade.exceptions import DependencyException, OperationalException
|
from freqtrade.exceptions import DependencyException, OperationalException
|
||||||
from freqtrade.exchange.common import remove_exchange_credentials
|
from freqtrade.exchange.common import remove_exchange_credentials
|
||||||
from freqtrade.misc import deep_merge_dicts, is_file_in_dir
|
from freqtrade.misc import deep_merge_dicts, is_file_in_dir
|
||||||
from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest,
|
from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestMetadataUpdate,
|
||||||
BacktestResponse)
|
BacktestRequest, BacktestResponse)
|
||||||
from freqtrade.rpc.api_server.deps import get_config
|
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
|
||||||
@@ -74,10 +75,11 @@ def __run_backtest_bg(btconfig: Config):
|
|||||||
ApiBG.bt['bt'].load_prior_backtest()
|
ApiBG.bt['bt'].load_prior_backtest()
|
||||||
|
|
||||||
ApiBG.bt['bt'].abort = False
|
ApiBG.bt['bt'].abort = False
|
||||||
|
strategy_name = strat.get_strategy_name()
|
||||||
if (ApiBG.bt['bt'].results and
|
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.
|
# 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:
|
else:
|
||||||
min_date, max_date = ApiBG.bt['bt'].backtest_one_strategy(
|
min_date, max_date = ApiBG.bt['bt'].backtest_one_strategy(
|
||||||
strat, ApiBG.bt['data'], ApiBG.bt['timerange'])
|
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)
|
min_date=min_date, max_date=max_date)
|
||||||
|
|
||||||
if btconfig.get('export', 'none') == 'trades':
|
if btconfig.get('export', 'none') == 'trades':
|
||||||
store_backtest_stats(
|
fn = store_backtest_stats(
|
||||||
btconfig['exportfilename'], ApiBG.bt['bt'].results,
|
btconfig['exportfilename'], ApiBG.bt['bt'].results,
|
||||||
datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
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.")
|
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)
|
delete_backtest_result(file_abs)
|
||||||
return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results')
|
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)
|
||||||
|
|||||||
@@ -526,6 +526,12 @@ class BacktestHistoryEntry(BaseModel):
|
|||||||
strategy: str
|
strategy: str
|
||||||
run_id: str
|
run_id: str
|
||||||
backtest_start_time: int
|
backtest_start_time: int
|
||||||
|
notes: Optional[str] = ''
|
||||||
|
|
||||||
|
|
||||||
|
class BacktestMetadataUpdate(BaseModel):
|
||||||
|
strategy: str
|
||||||
|
notes: str = ''
|
||||||
|
|
||||||
|
|
||||||
class SysInfo(BaseModel):
|
class SysInfo(BaseModel):
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ logger = logging.getLogger(__name__)
|
|||||||
# 2.29: Add /exchanges endpoint
|
# 2.29: Add /exchanges endpoint
|
||||||
# 2.30: new /pairlists endpoint
|
# 2.30: new /pairlists endpoint
|
||||||
# 2.31: new /backtest/history/ delete 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.
|
# Public API, requires no auth.
|
||||||
router_public = APIRouter()
|
router_public = APIRouter()
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class Discord(Webhook):
|
|||||||
self._format = 'json'
|
self._format = 'json'
|
||||||
self._retries = 1
|
self._retries = 1
|
||||||
self._retry_delay = 0.1
|
self._retry_delay = 0.1
|
||||||
|
self._timeout = self._config['discord'].get('timeout', 10)
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ def get_BacktestResultType_default() -> BacktestResultType:
|
|||||||
class BacktestHistoryEntryType(BacktestMetadataType):
|
class BacktestHistoryEntryType(BacktestMetadataType):
|
||||||
filename: str
|
filename: str
|
||||||
strategy: str
|
strategy: str
|
||||||
|
notes: str
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ numpy==1.24.3; python_version <= '3.8'
|
|||||||
pandas==2.0.3
|
pandas==2.0.3
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==4.0.47
|
ccxt==4.0.50
|
||||||
cryptography==41.0.3; platform_machine != 'armv7l'
|
cryptography==41.0.3; platform_machine != 'armv7l'
|
||||||
cryptography==40.0.1; platform_machine == 'armv7l'
|
cryptography==40.0.1; platform_machine == 'armv7l'
|
||||||
aiohttp==3.8.5
|
aiohttp==3.8.5
|
||||||
@@ -15,7 +15,7 @@ arrow==1.2.3
|
|||||||
cachetools==5.3.1
|
cachetools==5.3.1
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
urllib3==2.0.4
|
urllib3==2.0.4
|
||||||
jsonschema==4.18.5
|
jsonschema==4.18.6
|
||||||
TA-Lib==0.4.27
|
TA-Lib==0.4.27
|
||||||
technical==1.4.0
|
technical==1.4.0
|
||||||
tabulate==0.9.0
|
tabulate==0.9.0
|
||||||
@@ -33,13 +33,13 @@ 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.9.2
|
orjson==3.9.3
|
||||||
|
|
||||||
# Notify systemd
|
# Notify systemd
|
||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.100.1
|
fastapi==0.101.0
|
||||||
pydantic==1.10.11
|
pydantic==1.10.11
|
||||||
uvicorn==0.23.2
|
uvicorn==0.23.2
|
||||||
pyjwt==2.8.0
|
pyjwt==2.8.0
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
from freqtrade.enums.marginmode import MarginMode
|
from freqtrade.enums.marginmode import MarginMode
|
||||||
from freqtrade.enums.tradingmode import TradingMode
|
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.conftest import get_mock_coro, get_patched_exchange
|
||||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
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_count == 1
|
||||||
assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT'
|
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]
|
kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1]
|
||||||
assert kwargs['params'] == {}
|
|
||||||
assert kwargs['since'] is None
|
assert kwargs['since'] is None
|
||||||
|
|
||||||
api_mock.fetch_funding_rate_history.reset_mock()
|
api_mock.fetch_funding_rate_history.reset_mock()
|
||||||
since_ms = 1610000000000
|
since_ms = 1610000000000
|
||||||
since_ms_end = since_ms + (timeframe_to_msecs('4h') * limit)
|
|
||||||
# Test fetch_funding_rate_history (current data)
|
# Test fetch_funding_rate_history (current data)
|
||||||
await exchange._fetch_funding_rate_history(
|
await exchange._fetch_funding_rate_history(
|
||||||
pair='BTC/USDT:USDT',
|
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_count == 1
|
||||||
assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT'
|
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]
|
kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1]
|
||||||
assert kwargs['params'] == {'until': since_ms_end}
|
|
||||||
assert kwargs['since'] == since_ms
|
assert kwargs['since'] == since_ms
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -391,7 +391,7 @@ class TestCCXTExchange:
|
|||||||
assert po['id'] is not None
|
assert po['id'] is not None
|
||||||
if len(order.keys()) < 5:
|
if len(order.keys()) < 5:
|
||||||
# Kucoin case
|
# Kucoin case
|
||||||
assert po['status'] == 'closed'
|
assert po['status'] is None
|
||||||
continue
|
continue
|
||||||
assert po['timestamp'] == 1674493798550
|
assert po['timestamp'] == 1674493798550
|
||||||
assert isinstance(po['datetime'], str)
|
assert isinstance(po['datetime'], str)
|
||||||
@@ -511,7 +511,8 @@ class TestCCXTExchange:
|
|||||||
now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2))
|
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)
|
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)
|
timeframe_ms = timeframe_to_msecs(timeframe)
|
||||||
now = timeframe_to_prev_date(
|
now = timeframe_to_prev_date(
|
||||||
@@ -532,11 +533,11 @@ class TestCCXTExchange:
|
|||||||
assert res[1] == timeframe
|
assert res[1] == timeframe
|
||||||
assert res[2] == candle_type
|
assert res[2] == candle_type
|
||||||
candles = res[3]
|
candles = res[3]
|
||||||
factor = 0.9
|
|
||||||
candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * factor
|
candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * factor
|
||||||
candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms * factor
|
candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms * factor
|
||||||
assert len(candles) >= min(candle_count, candle_count1), \
|
assert len(candles) >= min(candle_count, candle_count1), \
|
||||||
f"{len(candles)} < {candle_count} in {timeframe}, Offset: {offset} {factor}"
|
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)
|
assert candles[0][0] == since_ms or (since_ms + timeframe_ms)
|
||||||
|
|
||||||
def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||||
@@ -544,8 +545,6 @@ class TestCCXTExchange:
|
|||||||
if exchangename in ('bittrex'):
|
if exchangename in ('bittrex'):
|
||||||
# For some weired reason, this test returns random lengths for bittrex.
|
# For some weired reason, this test returns random lengths for bittrex.
|
||||||
pytest.skip("Exchange doesn't provide stable ohlcv history")
|
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']:
|
if not exc._ft_has['ohlcv_has_history']:
|
||||||
pytest.skip("Exchange does not support candle history")
|
pytest.skip("Exchange does not support candle history")
|
||||||
@@ -554,15 +553,29 @@ class TestCCXTExchange:
|
|||||||
self.ccxt__async_get_candle_history(
|
self.ccxt__async_get_candle_history(
|
||||||
exc, exchangename, pair, timeframe, CandleType.SPOT)
|
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
|
exchange, exchangename = exchange_futures
|
||||||
if not exchange:
|
if not exchange:
|
||||||
# exchange_futures only returns values for supported exchanges
|
# exchange_futures only returns values for supported exchanges
|
||||||
return
|
return
|
||||||
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
||||||
timeframe = EXCHANGES[exchangename]['timeframe']
|
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(
|
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):
|
def test_ccxt_fetch_funding_rate_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
exchange, exchangename = exchange_futures
|
exchange, exchangename = exchange_futures
|
||||||
|
|||||||
@@ -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)
|
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
|
||||||
# one_call calculation * 1.8 should do 2 calls
|
# 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(
|
ret = exchange.get_historic_ohlcv(
|
||||||
pair,
|
pair,
|
||||||
"5m",
|
"5m",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from unittest.mock import ANY, MagicMock, PropertyMock
|
|||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
import rapidjson
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import FastAPI, WebSocketDisconnect
|
from fastapi import FastAPI, WebSocketDisconnect
|
||||||
from fastapi.exceptions import HTTPException
|
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):
|
def client_get(client: TestClient, url):
|
||||||
# Add fake Origin to ensure CORS kicks in
|
# Add fake Origin to ensure CORS kicks in
|
||||||
return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
|
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}")
|
rc = client_get(client, f"{BASE_URI}/pairlists/evaluate/{job_id}")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
response = rc.json()
|
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
|
assert response['result']['length'] == 4
|
||||||
|
|
||||||
# Restart with additional filter, reducing the list to 2
|
# 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
|
assert len(result) == 3
|
||||||
fn = result[0]['filename']
|
fn = result[0]['filename']
|
||||||
assert fn == "backtest-result_multistrat"
|
assert fn == "backtest-result_multistrat"
|
||||||
|
assert result[0]['notes'] == ''
|
||||||
strategy = result[0]['strategy']
|
strategy = result[0]['strategy']
|
||||||
rc = client_get(client, f"{BASE_URI}/backtest/history/result?filename={fn}&strategy={strategy}")
|
rc = client_get(client, f"{BASE_URI}/backtest/history/result?filename={fn}&strategy={strategy}")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
@@ -2018,7 +2030,7 @@ def test_api_backtest_history(botclient, mocker, testdatadir):
|
|||||||
assert result2['backtest_result']['strategy'][strategy]
|
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
|
ftbot, client = botclient
|
||||||
|
|
||||||
# Create a temporary directory and file
|
# 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()
|
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):
|
def test_health(botclient):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ class StrategyTestV3(IStrategy):
|
|||||||
|
|
||||||
if current_profit < -0.0075:
|
if current_profit < -0.0075:
|
||||||
orders = trade.select_filled_orders(trade.entry_side)
|
orders = trade.select_filled_orders(trade.entry_side)
|
||||||
return round(orders[0].safe_cost, 0)
|
return round(orders[0].stake_amount, 0)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -1501,9 +1501,9 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
|
|||||||
|
|
||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
def test_create_stoploss_order_invalid_order(
|
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)]
|
order = limit_order[exit_side(is_short)]
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@@ -1534,6 +1534,7 @@ def test_create_stoploss_order_invalid_order(
|
|||||||
trade = Trade.session.scalars(select(Trade)).first()
|
trade = Trade.session.scalars(select(Trade)).first()
|
||||||
trade.is_short = is_short
|
trade.is_short = is_short
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
rpc_mock.reset_mock()
|
||||||
freqtrade.create_stoploss_order(trade, 200)
|
freqtrade.create_stoploss_order(trade, 200)
|
||||||
assert trade.stoploss_order_id is None
|
assert trade.stoploss_order_id is None
|
||||||
assert trade.exit_reason == ExitType.EMERGENCY_EXIT.value
|
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
|
assert create_order_mock.call_args[1]['amount'] == trade.amount
|
||||||
|
|
||||||
# Rpc is sending first buy, then sell
|
# Rpc is sending first buy, then sell
|
||||||
assert rpc_mock.call_count == 3
|
assert rpc_mock.call_count == 2
|
||||||
assert rpc_mock.call_args_list[2][0][0]['sell_reason'] == ExitType.EMERGENCY_EXIT.value
|
assert rpc_mock.call_args_list[0][0][0]['sell_reason'] == ExitType.EMERGENCY_EXIT.value
|
||||||
assert rpc_mock.call_args_list[2][0][0]['order_type'] == 'market'
|
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])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
|
|||||||
Reference in New Issue
Block a user