mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge branch 'develop' into pr/Axel-CH/8779
This commit is contained in:
commit
88925d6c1d
|
@ -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.
|
||||
|
|
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).
|
||||
|
||||
## 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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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]:
|
||||
"""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
@ -25,3 +25,4 @@ def get_BacktestResultType_default() -> BacktestResultType:
|
|||
class BacktestHistoryEntryType(BacktestMetadataType):
|
||||
filename: str
|
||||
strategy: str
|
||||
notes: str
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
|
Loading…
Reference in New Issue
Block a user