Merge branch 'develop' into dataformat/feather

This commit is contained in:
Matthias 2023-07-29 20:04:12 +02:00
commit 6659d26131
86 changed files with 1387 additions and 411 deletions

View File

@ -461,7 +461,7 @@ jobs:
python setup.py sdist bdist_wheel
- name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.8.7
uses: pypa/gh-action-pypi-publish@v1.8.8
if: (github.event_name == 'release')
with:
user: __token__
@ -469,7 +469,7 @@ jobs:
repository_url: https://test.pypi.org/legacy/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.8.7
uses: pypa/gh-action-pypi-publish@v1.8.8
if: (github.event_name == 'release')
with:
user: __token__

View File

@ -13,12 +13,12 @@ repos:
- id: mypy
exclude: build_helpers
additional_dependencies:
- types-cachetools==5.3.0.5
- types-cachetools==5.3.0.6
- types-filelock==3.2.7
- types-requests==2.31.0.1
- types-tabulate==0.9.0.2
- types-python-dateutil==2.8.19.13
- SQLAlchemy==2.0.18
- types-requests==2.31.0.2
- types-tabulate==0.9.0.3
- types-python-dateutil==2.8.19.14
- SQLAlchemy==2.0.19
# stages: [push]
- repo: https://github.com/pycqa/isort

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,21 +1,11 @@
# Downloads don't work automatically, since the URL is regenerated via javascript.
# Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib
# vendored Wheels compiled via https://github.com/xmatthias/ta-lib-python/tree/ta_bundled_040
python -m pip install --upgrade pip wheel
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
if ($pyv -eq '3.8') {
pip install build_helpers\TA_Lib-0.4.26-cp38-cp38-win_amd64.whl
}
if ($pyv -eq '3.9') {
pip install build_helpers\TA_Lib-0.4.26-cp39-cp39-win_amd64.whl
}
if ($pyv -eq '3.10') {
pip install build_helpers\TA_Lib-0.4.26-cp310-cp310-win_amd64.whl
}
if ($pyv -eq '3.11') {
pip install build_helpers\TA_Lib-0.4.26-cp311-cp311-win_amd64.whl
}
pip install --find-links=build_helpers\ TA-Lib
pip install -r requirements-dev.txt
pip install -e .

View File

@ -32,5 +32,5 @@ services:
--logfile /freqtrade/user_data/logs/freqtrade.log
--db-url sqlite:////freqtrade/user_data/tradesv3.sqlite
--config /freqtrade/user_data/config.json
--freqai-model XGBoostClassifier
--strategy SampleStrategy
--freqaimodel XGBoostRegressor
--strategy FreqaiExampleStrategy

View File

@ -103,6 +103,22 @@ The indicators have to be present in your strategy's main DataFrame (either for
timeframe or for informative timeframes) otherwise they will simply be ignored in the script
output.
There are a range of candle and trade-related fields that are included in the analysis so are
automatically accessible by including them on the indicator-list, and these include:
- **open_date :** trade open datetime
- **close_date :** trade close datetime
- **min_rate :** minimum price seen throughout the position
- **max_rate :** maxiumum price seen throughout the position
- **open :** signal candle open price
- **close :** signal candle close price
- **high :** signal candle high price
- **low :** signal candle low price
- **volume :** signal candle volumne
- **profit_ratio :** trade profit ratio
- **profit_abs :** absolute profit return of the trade
### Filtering the trade output by date
To show only trades between dates within your backtested timerange, supply the usual `timerange` option in `YYYYMMDD-[YYYYMMDD]` format:

View File

@ -305,7 +305,7 @@ A backtesting result will look like that:
| Sharpe | 2.97 |
| Calmar | 6.29 |
| Profit factor | 1.11 |
| Expectancy | -0.15 |
| Expectancy (Ratio) | -0.15 (-0.05) |
| Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC |
| | |
@ -324,6 +324,7 @@ A backtesting result will look like that:
| Days win/draw/lose | 12 / 82 / 25 |
| Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 |
| Max Consecutive Wins / Loss | 3 / 4 |
| Rejected Entry signals | 3089 |
| Entry/Exit Timeouts | 0 / 0 |
| Canceled Trade Entries | 34 |
@ -409,7 +410,7 @@ It contains some useful key metrics about performance of your strategy on backte
| Sharpe | 2.97 |
| Calmar | 6.29 |
| Profit factor | 1.11 |
| Expectancy | -0.15 |
| Expectancy (Ratio) | -0.15 (-0.05) |
| Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC |
| | |
@ -428,6 +429,7 @@ It contains some useful key metrics about performance of your strategy on backte
| Days win/draw/lose | 12 / 82 / 25 |
| Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 |
| Max Consecutive Wins / Loss | 3 / 4 |
| Rejected Entry signals | 3089 |
| Entry/Exit Timeouts | 0 / 0 |
| Canceled Trade Entries | 34 |
@ -467,6 +469,7 @@ It contains some useful key metrics about performance of your strategy on backte
- `Best day` / `Worst day`: Best and worst day based on daily profit.
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
- `Max Consecutive Wins / Loss`: Maximum consecutive wins/losses in a row.
- `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached.
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
- `Canceled Trade Entries`: Number of trades that have been canceled by user request via `adjust_entry_price`.
@ -534,6 +537,7 @@ Since backtesting lacks some detailed information about what happens within a ca
- ROI
- exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%)
- exits are never "below the candle", so a ROI of 2% may result in a exit at 2.4% if low was at 2.4% profit
- ROI entries which came into effect on the triggering candle (e.g. `120: 0.02` for 1h candles, from `60: 0.05`) will use the candle's open as exit rate
- Force-exits caused by `<N>=-1` ROI entries use low as exit value, unless N falls on the candle open (e.g. `120: -1` for 1h candles)
- Stoploss exits happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price
- Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` exit reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes

View File

@ -259,10 +259,17 @@ The configuration parameter `exchange.unknown_fee_rate` can be used to specify t
Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode.
Users with unified accounts (there's no way back) can create a Sub-account which will start as "non-unified", and can therefore use isolated futures.
On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors.
On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors
As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well.
API Keys for live futures trading (Subaccount on non-unified) must have the following permissions:
* Read-write
* Contract - Orders
* Contract - Positions
We do strongly recommend to limit all API keys to the IP you're going to use it from.
!!! Tip "Stoploss on Exchange"
Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.

View File

@ -261,7 +261,7 @@ class MyFreqaiModel(BaseRegressionModel):
"""
feature_pipeline = Pipeline([
('qt', SKLearnWrapper(QuantileTransformer(output_distribution='normal'))),
('di', ds.DissimilarityIndex(di_threshold=1)
('di', ds.DissimilarityIndex(di_threshold=1))
])
return feature_pipeline

View File

@ -42,7 +42,6 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| `use_SVM_to_remove_outliers` | Train a support vector machine to detect and remove outliers from the training dataset, as well as from incoming data points. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm). <br> **Datatype:** Boolean.
| `svm_params` | All parameters available in Sklearn's `SGDOneClassSVM()`. See details about some select parameters [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm). <br> **Datatype:** Dictionary.
| `use_DBSCAN_to_remove_outliers` | Cluster data using the DBSCAN algorithm to identify and remove outliers from training and prediction data. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan). <br> **Datatype:** Boolean.
| `inlier_metric_window` | If set, FreqAI adds an `inlier_metric` to the training feature set and set the lookback to be the `inlier_metric_window`, i.e., the number of previous time points to compare the current candle to. Details of how the `inlier_metric` is computed can be found [here](freqai-feature-engineering.md#inlier-metric). <br> **Datatype:** Integer. <br> Default: `0`.
| `noise_standard_deviation` | If set, FreqAI adds noise to the training features with the aim of preventing overfitting. FreqAI generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. `noise_standard_deviation` should be kept relative to the normalized space, i.e., between -1 and 1. In other words, since data in FreqAI is always normalized to be between -1 and 1, `noise_standard_deviation: 0.05` would result in 32% of the data being randomly increased/decreased by more than 2.5% (i.e., the percent of data falling within the first standard deviation). <br> **Datatype:** Integer. <br> Default: `0`.
| `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset. <br> **Datatype:** Float. <br> Default: `30`.
| `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, you should be careful to understand the unorthodox nature of this parameter before employing it. <br> **Datatype:** Boolean. <br> Default: `False` (no reversal).

View File

@ -211,7 +211,7 @@ The user is responsible for providing a server or local file that returns a JSON
```json
{
"pairs": ["XRP/USDT", "ETH/USDT", "LTC/USDT"],
"refresh_period": 1800,
"refresh_period": 1800
}
```

11
docs/includes/showcase.md Normal file
View File

@ -0,0 +1,11 @@
This section will highlight a few projects from members of the community.
!!! Note
The projects below are for the most part not maintained by the freqtrade , therefore use your own caution before using them.
- [Example freqtrade strategies](https://github.com/freqtrade/freqtrade-strategies/)
- [FrequentHippo - Grafana dashboard with dry/live runs and backtests](http://frequenthippo.ddns.net:3000/) (by hippocritical).
- [Online pairlist generator](https://remotepairlist.com/) (by Blood4rc).
- [Freqtrade Backtesting Project](https://bt.robot.co.network/) (by Blood4rc).
- [Freqtrade analysis notebook](https://github.com/froggleston/freqtrade_analysis_notebook) (by Froggleston).
- [TUI for freqtrade](https://github.com/froggleston/freqtrade-frogtrade9000) (by Froggleston).
- [Bot Academy](https://botacademy.ddns.net/) (by stash86) - Blog about crypto bot projects.

View File

@ -63,6 +63,10 @@ Exchanges confirmed working by the community:
- [X] [Bitvavo](https://bitvavo.com/)
- [X] [Kucoin](https://www.kucoin.com/)
## Community showcase
--8<-- "includes/showcase.md"
## Requirements
### Hardware requirements

View File

@ -1,6 +1,6 @@
markdown==3.3.7
mkdocs==1.4.3
mkdocs-material==9.1.18
mkdocs-material==9.1.19
mdx_truly_sane_lists==1.3
pymdown-extensions==10.0.1
pymdown-extensions==10.1
jinja2==3.1.2

View File

@ -750,7 +750,7 @@ class DigDeeperStrategy(IStrategy):
# Hope you have a deep wallet!
try:
# This returns first order stake size
stake_amount = filled_entries[0].cost
stake_amount = filled_entries[0].stake_amount
# This then calculates current safety order size
stake_amount = stake_amount * (1 + (count_of_entries * 0.25))
return stake_amount

View File

@ -287,12 +287,17 @@ Return a summary of your profit/loss and performance.
> **Best Performing:** `PAY/BTC: 50.23%`
> **Trading volume:** `0.5 BTC`
> **Profit factor:** `1.04`
> **Win / Loss:** `102 / 36`
> **Winrate:** `73.91%`
> **Expectancy (Ratio):** `4.87 (1.66)`
> **Max Drawdown:** `9.23% (0.01255 BTC)`
The relative profit of `1.2%` is the average profit per trade.
The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`.
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy.
Expectancy corresponds to the average return per currency unit at risk, i.e. the winrate and the risk-reward ratio (the average gain of winning trades compared to the average loss of losing trades).
Expectancy Ratio is expected profit or loss of a subsequent trade based on the performance of all past trades.
Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
Bot started date will refer to the date the bot was first started. For older bots, this will default to the first trade's open date.

View File

@ -141,7 +141,8 @@ Most properties here can be None as they are dependant on the exchange response.
`amount` | float | Amount in base currency
`filled` | float | Filled amount (in base currency)
`remaining` | float | Remaining amount
`cost` | float | Cost of the order - usually average * filled
`cost` | float | Cost of the order - usually average * filled (*Exchange dependant on futures, may contain the cost with or without leverage and may be in contracts.*)
`stake_amount` | float | Stake amount used for this order. *Added in 2023.7.*
`order_date` | datetime | Order creation date **use `order_date_utc` instead**
`order_date_utc` | datetime | Order creation date (in UTC)
`order_fill_date` | datetime | Order fill date **use `order_fill_utc` instead**

View File

@ -80,12 +80,18 @@ When using the Form-Encoded or JSON-Encoded configuration you can configure any
The result would be a POST request with e.g. `Status: running` body and `Content-Type: text/plain` header.
Optional parameters are available to enable automatic retries for webhook messages. The `webhook.retries` parameter can be set for the maximum number of retries the webhook request should attempt if it is unsuccessful (i.e. HTTP response status is not 200). By default this is set to `0` which is disabled. An additional `webhook.retry_delay` parameter can be set to specify the time in seconds between retry attempts. By default this is set to `0.1` (i.e. 100ms). Note that increasing the number of retries or retry delay may slow down the trader if there are connectivity issues with the webhook. Example configuration for retries:
## Additional configurations
The `webhook.retries` parameter can be set for the maximum number of retries the webhook request should attempt if it is unsuccessful (i.e. HTTP response status is not 200). By default this is set to `0` which is disabled. An additional `webhook.retry_delay` parameter can be set to specify the time in seconds between retry attempts. By default this is set to `0.1` (i.e. 100ms). Note that increasing the number of retries or retry delay may slow down the trader if there are connectivity issues with the webhook.
You can also specify `webhook.timeout` - which defines how long the bot will wait until it assumes the other host as unresponsive (defaults to 10s).
Example configuration for retries:
```json
"webhook": {
"enabled": true,
"url": "https://<YOURHOOKURL>",
"timeout": 10,
"retries": 3,
"retry_delay": 0.2,
"status": {
@ -109,6 +115,8 @@ Custom messages can be sent to Webhook endpoints via the `self.dp.send_msg()` fu
Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called.
## Webhook Message types
### Entry
The fields in `webhook.entry` are filled when the bot executes a long/short. Parameters are filled using string.format.

View File

@ -1,5 +1,5 @@
""" Freqtrade bot """
__version__ = '2023.7.dev'
__version__ = '2023.8.dev'
if 'dev' in __version__:
from pathlib import Path

View File

@ -5,6 +5,7 @@ from typing import Any, Dict, List
from questionary import Separator, prompt
from freqtrade.configuration.detect_environment import running_in_docker
from freqtrade.configuration.directory_operations import chown_user_directory
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
from freqtrade.exceptions import OperationalException
@ -179,7 +180,7 @@ def ask_user_config() -> Dict[str, Any]:
"name": "api_server_listen_addr",
"message": ("Insert Api server Listen Address (0.0.0.0 for docker, "
"otherwise best left untouched)"),
"default": "127.0.0.1",
"default": "127.0.0.1" if not running_in_docker() else "0.0.0.0",
"when": lambda x: x['api_server']
},
{

View File

@ -0,0 +1,8 @@
import os
def running_in_docker() -> bool:
"""
Check if we are running in a docker container
"""
return os.environ.get('FT_APP_ENV') == 'docker'

View File

@ -3,6 +3,7 @@ import shutil
from pathlib import Path
from typing import Optional
from freqtrade.configuration.detect_environment import running_in_docker
from freqtrade.constants import (USER_DATA_FILES, USERPATH_FREQAIMODELS, USERPATH_HYPEROPTS,
USERPATH_NOTEBOOKS, USERPATH_STRATEGIES, Config)
from freqtrade.exceptions import OperationalException
@ -30,8 +31,7 @@ def chown_user_directory(directory: Path) -> None:
Use Sudo to change permissions of the home-directory if necessary
Only applies when running in docker!
"""
import os
if os.environ.get('FT_APP_ENV') == 'docker':
if running_in_docker():
try:
import subprocess
subprocess.check_output(

View File

@ -170,6 +170,7 @@ def load_and_merge_backtest_result(strategy_name: str, filename: Path, results:
def _get_backtest_files(dirname: Path) -> List[Path]:
# Weird glob expression here avoids including .meta.json files.
return list(reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json'))))
@ -184,7 +185,7 @@ def get_backtest_resultlist(dirname: Path):
continue
for s, v in metadata.items():
results.append({
'filename': filename.name,
'filename': filename.stem,
'strategy': s,
'run_id': v['run_id'],
'backtest_start_time': v['backtest_start_time'],
@ -193,6 +194,17 @@ def get_backtest_resultlist(dirname: Path):
return results
def delete_backtest_result(file_abs: Path):
"""
Delete backtest result file and corresponding metadata file.
"""
# *.meta.json
logger.info(f"Deleting backtest result file: {file_abs.name}")
file_abs_meta = file_abs.with_suffix('.meta.json')
file_abs.unlink()
file_abs_meta.unlink()
def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str],
min_backtest_date: Optional[datetime] = None) -> Dict[str, Any]:
"""
@ -211,7 +223,6 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
'strategy_comparison': [],
}
# Weird glob expression here avoids including .meta.json files.
for filename in _get_backtest_files(dirname):
metadata = load_backtest_metadata(filename)
if not metadata:

View File

@ -96,8 +96,14 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
'volume': 'sum'
}
timeframe_minutes = timeframe_to_minutes(timeframe)
resample_interval = f'{timeframe_minutes}min'
if timeframe_minutes >= 43200 and timeframe_minutes < 525600:
# Monthly candles need special treatment to stick to the 1st of the month
resample_interval = f'{timeframe}S'
elif timeframe_minutes > 43200:
resample_interval = timeframe
# Resample to create "NAN" values
df = dataframe.resample(f'{timeframe_minutes}min', on='date').agg(ohlcv_dict)
df = dataframe.resample(resample_interval, on='date').agg(ohlcv_dict)
# Forwardfill close for missing columns
df['close'] = df['close'].fillna(method='ffill')
@ -122,7 +128,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
return df
def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date',
def trim_dataframe(df: DataFrame, timerange, *, df_date_col: str = 'date',
startup_candles: int = 0) -> DataFrame:
"""
Trim dataframe based on given timerange

View File

@ -194,32 +194,35 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
def calculate_expectancy(trades: pd.DataFrame) -> float:
def calculate_expectancy(trades: pd.DataFrame) -> Tuple[float, float]:
"""
Calculate expectancy
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
:return: expectancy
:return: expectancy, expectancy_ratio
"""
if len(trades) == 0:
return 0
expectancy = 1
expectancy = 0
expectancy_ratio = 100
profit_sum = trades.loc[trades['profit_abs'] > 0, 'profit_abs'].sum()
loss_sum = abs(trades.loc[trades['profit_abs'] < 0, 'profit_abs'].sum())
nb_win_trades = len(trades.loc[trades['profit_abs'] > 0])
nb_loss_trades = len(trades.loc[trades['profit_abs'] < 0])
if len(trades) > 0:
winning_trades = trades.loc[trades['profit_abs'] > 0]
losing_trades = trades.loc[trades['profit_abs'] < 0]
profit_sum = winning_trades['profit_abs'].sum()
loss_sum = abs(losing_trades['profit_abs'].sum())
nb_win_trades = len(winning_trades)
nb_loss_trades = len(losing_trades)
if (nb_win_trades > 0) and (nb_loss_trades > 0):
average_win = profit_sum / nb_win_trades
average_loss = loss_sum / nb_loss_trades
risk_reward_ratio = average_win / average_loss
winrate = nb_win_trades / len(trades)
expectancy = ((1 + risk_reward_ratio) * winrate) - 1
elif nb_win_trades == 0:
expectancy = 0
average_win = (profit_sum / nb_win_trades) if nb_win_trades > 0 else 0
average_loss = (loss_sum / nb_loss_trades) if nb_loss_trades > 0 else 0
winrate = (nb_win_trades / len(trades))
loserate = (nb_loss_trades / len(trades))
return expectancy
expectancy = (winrate * average_win) - (loserate * average_loss)
if (average_loss > 0):
risk_reward_ratio = average_win / average_loss
expectancy_ratio = ((1 + risk_reward_ratio) * winrate) - 1
return expectancy, expectancy_ratio
def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: datetime,

View File

@ -172,13 +172,7 @@ class Edge:
pair_data = pair_data.sort_values(by=['date'])
pair_data = pair_data.reset_index(drop=True)
df_analyzed = self.strategy.advise_exit(
dataframe=self.strategy.advise_entry(
dataframe=pair_data,
metadata={'pair': pair}
),
metadata={'pair': pair}
)[headers].copy()
df_analyzed = self.strategy.ft_advise_signals(pair_data, {'pair': pair})[headers].copy()
trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range)

View File

@ -34,6 +34,7 @@ class Binance(Exchange):
"tickers_have_price": False,
"floor_leverage": True,
"stop_price_type_field": "workingType",
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
"stop_price_type_value_mapping": {
PriceType.LAST: "CONTRACT_PRICE",
PriceType.MARK: "MARK_PRICE",

File diff suppressed because it is too large Load Diff

View File

@ -80,9 +80,8 @@ class Exchange:
"mark_ohlcv_price": "mark",
"mark_ohlcv_timeframe": "8h",
"ccxt_futures_name": "swap",
"fee_cost_in_contracts": False, # Fee cost needs contract conversion
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
# Override createMarketBuyOrderRequiresPrice where ccxt has it wrong
"marketOrderRequiresPrice": False,
}
@ -1859,9 +1858,6 @@ class Exchange:
if fee_curr is None:
return None
fee_cost = float(fee['cost'])
if self._ft_has['fee_cost_in_contracts']:
# Convert cost via "contracts" conversion
fee_cost = self._contracts_to_amount(symbol, fee['cost'])
# Calculate fee based on order details
if fee_curr == self.get_pair_base_currency(symbol):

View File

@ -33,8 +33,6 @@ class Gate(Exchange):
_ft_has_futures: Dict = {
"needs_trading_fees": True,
"marketOrderRequiresPrice": False,
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
"stop_price_type_field": "price_type",
"stop_price_type_value_mapping": {
PriceType.LAST: 0,

View File

@ -32,7 +32,6 @@ class Okx(Exchange):
}
_ft_has_futures: Dict = {
"tickers_have_quoteVolume": False,
"fee_cost_in_contracts": True,
"stop_price_type_field": "slTriggerPxType",
"stop_price_type_value_mapping": {
PriceType.LAST: "last",

View File

@ -86,8 +86,6 @@ class IFreqaiModel(ABC):
logger.warning("DI threshold is not configured for Keras models yet. Deactivating.")
self.CONV_WIDTH = self.freqai_info.get('conv_width', 1)
if self.ft_params.get("inlier_metric_window", 0):
self.CONV_WIDTH = self.ft_params.get("inlier_metric_window", 0) * 2
self.class_names: List[str] = [] # used in classification subclasses
self.pair_it = 0
self.pair_it_train = 0
@ -676,15 +674,6 @@ class IFreqaiModel(ABC):
hist_preds_df['close_price'] = strat_df['close']
hist_preds_df['date_pred'] = strat_df['date']
# # for keras type models, the conv_window needs to be prepended so
# # viewing is correct in frequi
if self.ft_params.get('inlier_metric_window', 0):
n_lost_points = self.freqai_info.get('conv_width', 2)
zeros_df = DataFrame(np.zeros((n_lost_points, len(hist_preds_df.columns))),
columns=hist_preds_df.columns)
self.dd.historic_predictions[pair] = pd.concat(
[zeros_df, hist_preds_df], axis=0, ignore_index=True)
def fit_live_predictions(self, dk: FreqaiDataKitchen, pair: str) -> None:
"""
Fit the labels with a gaussian distribution

View File

@ -32,8 +32,8 @@ class LightGBMClassifier(BaseClassifierModel):
eval_set = None
test_weights = None
else:
eval_set = (data_dictionary["test_features"].to_numpy(),
data_dictionary["test_labels"].to_numpy()[:, 0])
eval_set = [(data_dictionary["test_features"].to_numpy(),
data_dictionary["test_labels"].to_numpy()[:, 0])]
test_weights = data_dictionary["test_weights"]
X = data_dictionary["train_features"].to_numpy()
y = data_dictionary["train_labels"].to_numpy()[:, 0]
@ -42,7 +42,6 @@ class LightGBMClassifier(BaseClassifierModel):
init_model = self.get_init_model(dk.pair)
model = LGBMClassifier(**self.model_training_parameters)
model.fit(X=X, y=y, eval_set=eval_set, sample_weight=train_weights,
eval_sample_weight=[test_weights], init_model=init_model)

View File

@ -32,7 +32,7 @@ class LightGBMRegressor(BaseRegressionModel):
eval_set = None
eval_weights = None
else:
eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"])
eval_set = [(data_dictionary["test_features"], data_dictionary["test_labels"])]
eval_weights = data_dictionary["test_weights"]
X = data_dictionary["train_features"]
y = data_dictionary["train_labels"]

View File

@ -42,10 +42,10 @@ class LightGBMRegressorMultiTarget(BaseRegressionModel):
eval_weights = [data_dictionary["test_weights"]]
eval_sets = [(None, None)] * data_dictionary['test_labels'].shape[1] # type: ignore
for i in range(data_dictionary['test_labels'].shape[1]):
eval_sets[i] = ( # type: ignore
eval_sets[i] = [( # type: ignore
data_dictionary["test_features"],
data_dictionary["test_labels"].iloc[:, i]
)
)]
init_model = self.get_init_model(dk.pair)
if init_model:

View File

@ -1383,7 +1383,10 @@ class FreqtradeBot(LoggingMixin):
latest_candle_close_date = timeframe_to_next_date(self.strategy.timeframe,
latest_candle_open_date)
# Check if new candle
if order_obj and latest_candle_close_date > order_obj.order_date_utc:
if (
order_obj and order_obj.side == trade.entry_side
and latest_candle_close_date > order_obj.order_date_utc
):
# New candle
proposed_rate = self.exchange.get_rate(
trade.pair, side='entry', is_short=trade.is_short, refresh=True)
@ -1939,6 +1942,7 @@ class FreqtradeBot(LoggingMixin):
"""
Applies the fee to amount (either from Order or from Trades).
Can eat into dust if more than the required asset is available.
In case of trade adjustment orders, trade.amount will not have been adjusted yet.
Can't happen in Futures mode - where Fees are always in settlement currency,
never in base currency.
"""
@ -1948,6 +1952,10 @@ class FreqtradeBot(LoggingMixin):
# check against remaining amount!
amount_ = trade.amount - amount
if trade.nr_of_successful_entries >= 1 and order_obj.ft_order_side == trade.entry_side:
# In case of rebuy's, trade.amount doesn't contain the amount of the last entry.
amount_ = trade.amount + amount
if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount_:
# Eat into dust if we own more than base currency
logger.info(f"Fee amount for {trade} was in base currency - "
@ -1977,7 +1985,11 @@ class FreqtradeBot(LoggingMixin):
# Init variables
order_amount = safe_value_fallback(order, 'filled', 'amount')
# Only run for closed orders
if trade.fee_updated(order.get('side', '')) or order['status'] == 'open':
if (
trade.fee_updated(order.get('side', ''))
or order['status'] == 'open'
or order_obj.ft_fee_base
):
return None
trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)

View File

@ -116,6 +116,13 @@ def file_load_json(file: Path):
return pairdata
def is_file_in_dir(file: Path, directory: Path) -> bool:
"""
Helper function to check if file is in directory.
"""
return file.is_file() and file.parent.samefile(directory)
def pair_to_filename(pair: str) -> str:
for ch in ['/', ' ', '.', '@', '$', '+', ':']:
pair = pair.replace(ch, '_')

View File

@ -367,11 +367,7 @@ class Backtesting:
if not pair_data.empty:
# Cleanup from prior runs
pair_data.drop(HEADERS[5:] + ['buy', 'sell'], axis=1, errors='ignore')
df_analyzed = self.strategy.advise_exit(
self.strategy.advise_entry(pair_data, {'pair': pair}),
{'pair': pair}
).copy()
df_analyzed = self.strategy.ft_advise_signals(pair_data, {'pair': pair})
# Trim startup period from analyzed dataframe
df_analyzed = processed[pair] = pair_data = trim_dataframe(
df_analyzed, self.timerange, startup_candles=self.required_startup)
@ -679,6 +675,7 @@ class Backtesting:
remaining=amount,
cost=amount * close_rate,
)
order._trade_bt = trade
trade.orders.append(order)
return trade
@ -901,8 +898,9 @@ class Backtesting:
amount=amount,
filled=0,
remaining=amount,
cost=stake_amount + trade.fee_open,
cost=amount * propose_rate + trade.fee_open,
)
order._trade_bt = trade
trade.orders.append(order)
if pos_adjust and self._get_order_filled(order.ft_price, row):
order.close_bt_order(current_time, trade)
@ -1275,6 +1273,7 @@ class Backtesting:
preprocessed = self.strategy.advise_all_indicators(data)
# Trim startup period from analyzed dataframe
# This only used to determine if trimming would result in an empty dataframe
preprocessed_tmp = trim_dataframes(preprocessed, timerange, self.required_startup)
if not preprocessed_tmp:

View File

@ -446,6 +446,8 @@ class Hyperopt:
preprocessed = self.backtesting.strategy.advise_all_indicators(data)
# Trim startup period from analyzed dataframe to get correct dates for output.
# This is only used to keep track of min/max date after trimming.
# The result is NOT returned from this method, actual trimming happens in backtesting.
trimmed = trim_dataframes(preprocessed, self.timerange, self.backtesting.required_startup)
self.min_date, self.max_date = get_timerange(trimmed)
if not self.market_change:

View File

@ -432,12 +432,10 @@ class HyperoptTools:
for i in range(len(trials)):
if trials.loc[i]['is_profit']:
for j in range(len(trials.loc[i]) - 3):
trials.iat[i, j] = "{}{}{}".format(Fore.GREEN,
str(trials.loc[i][j]), Fore.RESET)
trials.iat[i, j] = f"{Fore.GREEN}{str(trials.loc[i][j])}{Fore.RESET}"
if trials.loc[i]['is_best'] and highlight_best:
for j in range(len(trials.loc[i]) - 3):
trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT,
str(trials.loc[i][j]), Style.RESET_ALL)
trials.iat[i, j] = f"{Style.BRIGHT}{str(trials.loc[i][j])}{Style.RESET_ALL}"
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit', 'is_random'])
if remove_header > 0:

View File

@ -1,5 +1,6 @@
# flake8: noqa: F401
from freqtrade.optimize.optimize_reports.bt_output import (generate_edge_table,
generate_wins_draws_losses,
show_backtest_result,
show_backtest_results,
show_sorted_pairlist,
@ -14,5 +15,4 @@ from freqtrade.optimize.optimize_reports.optimize_reports import (
generate_all_periodic_breakdown_stats, generate_backtest_stats, generate_daily_stats,
generate_exit_reason_stats, generate_pair_metrics, generate_periodic_breakdown_stats,
generate_rejected_signals, generate_strategy_comparison, generate_strategy_stats,
generate_tag_metrics, generate_trade_signal_candles, generate_trading_stats,
generate_wins_draws_losses)
generate_tag_metrics, generate_trade_signal_candles, generate_trading_stats)

View File

@ -5,8 +5,7 @@ from tabulate import tabulate
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config
from freqtrade.misc import decimals_per_coin, round_coin_value
from freqtrade.optimize.optimize_reports.optimize_reports import (generate_periodic_breakdown_stats,
generate_wins_draws_losses)
from freqtrade.optimize.optimize_reports.optimize_reports import generate_periodic_breakdown_stats
logger = logging.getLogger(__name__)
@ -30,6 +29,16 @@ def _get_line_header(first_column: str, stake_currency: str,
'Win Draw Loss Win%']
def generate_wins_draws_losses(wins, draws, losses):
if wins > 0 and losses == 0:
wl_ratio = '100'
elif wins == 0:
wl_ratio = '0'
else:
wl_ratio = f'{100.0 / (wins + draws + losses) * wins:.1f}' if losses > 0 else '100'
return f'{wins:>4} {draws:>4} {losses:>4} {wl_ratio:>4}'
def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: str) -> str:
"""
Generates and returns a text table for the given backtest data and the results dataframe
@ -233,8 +242,9 @@ def text_table_add_metrics(strat_results: Dict) -> str:
('Calmar', f"{strat_results['calmar']:.2f}" if 'calmar' in strat_results else 'N/A'),
('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor'
in strat_results else 'N/A'),
('Expectancy', f"{strat_results['expectancy']:.2f}" if 'expectancy'
in strat_results else 'N/A'),
('Expectancy (Ratio)', (
f"{strat_results['expectancy']:.2f} ({strat_results['expectancy_ratio']:.2f})" if
'expectancy_ratio' in strat_results else 'N/A')),
('Trades per day', strat_results['trades_per_day']),
('Avg. daily profit %',
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),
@ -260,6 +270,9 @@ def text_table_add_metrics(strat_results: Dict) -> str:
f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
('Max Consecutive Wins / Loss',
f"{strat_results['max_consecutive_wins']} / {strat_results['max_consecutive_losses']}"
if 'max_consecutive_losses' in strat_results else 'N/A'),
('Rejected Entry signals', strat_results.get('rejected_signals', 'N/A')),
('Entry/Exit Timeouts',
f"{strat_results.get('timedout_entry_orders', 'N/A')} / "

View File

@ -1,9 +1,10 @@
import logging
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Union
from typing import Any, Dict, List, Tuple, Union
from pandas import DataFrame, concat, to_datetime
import numpy as np
from pandas import DataFrame, Series, concat, to_datetime
from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, IntOrInf
from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
@ -57,16 +58,6 @@ def generate_rejected_signals(preprocessed_df: Dict[str, DataFrame],
return rejected_candles_only
def generate_wins_draws_losses(wins, draws, losses):
if wins > 0 and losses == 0:
wl_ratio = '100'
elif wins == 0:
wl_ratio = '0'
else:
wl_ratio = f'{100.0 / (wins + draws + losses) * wins:.1f}' if losses > 0 else '100'
return f'{wins:>4} {draws:>4} {losses:>4} {wl_ratio:>4}'
def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict:
"""
Generate one result dict, with "first_column" as key.
@ -97,6 +88,7 @@ def _generate_result_line(result: DataFrame, starting_balance: int, first_column
'wins': len(result[result['profit_abs'] > 0]),
'draws': len(result[result['profit_abs'] == 0]),
'losses': len(result[result['profit_abs'] < 0]),
'winrate': len(result[result['profit_abs'] > 0]) / len(result) if len(result) else 0.0,
}
@ -184,6 +176,7 @@ def generate_exit_reason_stats(max_open_trades: IntOrInf, results: DataFrame) ->
'wins': len(result[result['profit_abs'] > 0]),
'draws': len(result[result['profit_abs'] == 0]),
'losses': len(result[result['profit_abs'] < 0]),
'winrate': len(result[result['profit_abs'] > 0]) / count if count else 0.0,
'profit_mean': profit_mean,
'profit_mean_pct': round(profit_mean * 100, 2),
'profit_sum': profit_sum,
@ -238,6 +231,7 @@ def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dic
wins = sum(day['profit_abs'] > 0)
draws = sum(day['profit_abs'] == 0)
loses = sum(day['profit_abs'] < 0)
trades = (wins + draws + loses)
stats.append(
{
'date': name.strftime('%d/%m/%Y'),
@ -245,7 +239,8 @@ def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dic
'profit_abs': profit_abs,
'wins': wins,
'draws': draws,
'loses': loses
'loses': loses,
'winrate': wins / trades if trades else 0.0,
}
)
return stats
@ -258,6 +253,23 @@ def generate_all_periodic_breakdown_stats(trade_list: List) -> Dict[str, List]:
return result
def calc_streak(dataframe: DataFrame) -> Tuple[int, int]:
"""
Calculate consecutive win and loss streaks
:param dataframe: Dataframe containing the trades dataframe, with profit_ratio column
:return: Tuple containing consecutive wins and losses
"""
df = Series(np.where(dataframe['profit_ratio'] > 0, 'win', 'loss')).to_frame('result')
df['streaks'] = df['result'].ne(df['result'].shift()).cumsum().rename('streaks')
df['counter'] = df['streaks'].groupby(df['streaks']).cumcount() + 1
res = df.groupby(df['result']).max()
#
cons_wins = int(res.loc['win', 'counter']) if 'win' in res.index else 0
cons_losses = int(res.loc['loss', 'counter']) if 'loss' in res.index else 0
return cons_wins, cons_losses
def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
""" Generate overall trade statistics """
if len(results) == 0:
@ -265,9 +277,12 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
'wins': 0,
'losses': 0,
'draws': 0,
'winrate': 0,
'holding_avg': timedelta(),
'winner_holding_avg': timedelta(),
'loser_holding_avg': timedelta(),
'max_consecutive_wins': 0,
'max_consecutive_losses': 0,
}
winning_trades = results.loc[results['profit_ratio'] > 0]
@ -280,17 +295,21 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
if not winning_trades.empty else timedelta())
loser_holding_avg = (timedelta(minutes=round(losing_trades['trade_duration'].mean()))
if not losing_trades.empty else timedelta())
winstreak, loss_streak = calc_streak(results)
return {
'wins': len(winning_trades),
'losses': len(losing_trades),
'draws': len(draw_trades),
'winrate': len(winning_trades) / len(results) if len(results) else 0.0,
'holding_avg': holding_avg,
'holding_avg_s': holding_avg.total_seconds(),
'winner_holding_avg': winner_holding_avg,
'winner_holding_avg_s': winner_holding_avg.total_seconds(),
'loser_holding_avg': loser_holding_avg,
'loser_holding_avg_s': loser_holding_avg.total_seconds(),
'max_consecutive_wins': winstreak,
'max_consecutive_losses': loss_streak,
}
@ -383,6 +402,7 @@ def generate_strategy_stats(pairlist: List[str],
losing_profit = results.loc[results['profit_abs'] < 0, 'profit_abs'].sum()
profit_factor = winning_profit / abs(losing_profit) if losing_profit else 0.0
expectancy, expectancy_ratio = calculate_expectancy(results)
backtest_days = (max_date - min_date).days or 1
strat_stats = {
'trades': results.to_dict(orient='records'),
@ -408,7 +428,8 @@ def generate_strategy_stats(pairlist: List[str],
'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(),
'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(),
'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']),
'expectancy': calculate_expectancy(results),
'expectancy': expectancy,
'expectancy_ratio': expectancy_ratio,
'sortino': calculate_sortino(results, min_date, max_date, start_balance),
'sharpe': calculate_sharpe(results, min_date, max_date, start_balance),
'calmar': calculate_calmar(results, min_date, max_date, start_balance),

View File

@ -38,6 +38,7 @@ class Order(ModelBase):
Mirrors CCXT Order structure
"""
__tablename__ = 'orders'
__allow_unmapped__ = True
session: ClassVar[SessionType]
# Uniqueness should be ensured over pair, order_id
@ -47,7 +48,8 @@ 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: Mapped["Trade"] = relationship("Trade", back_populates="orders")
_trade_live: Mapped["Trade"] = relationship("Trade", back_populates="orders")
_trade_bt: "LocalTrade" = None # type: ignore
# order_side can only be 'buy', 'sell' or 'stoploss'
ft_order_side: Mapped[str] = mapped_column(String(25), nullable=False)
@ -119,6 +121,15 @@ class Order(ModelBase):
def safe_amount_after_fee(self) -> float:
return self.safe_filled - self.safe_fee_base
@property
def trade(self) -> "LocalTrade":
return self._trade_bt or self._trade_live
@property
def stake_amount(self) -> float:
""" Amount in stake currency used for this order"""
return self.safe_amount * self.safe_price / self.trade.leverage
def __repr__(self):
return (f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, "
@ -1299,9 +1310,12 @@ class Trade(ModelBase, LocalTrade):
Float(), nullable=True, default=None) # type: ignore
def __init__(self, **kwargs):
from_json = kwargs.pop('__FROM_JSON', None)
super().__init__(**kwargs)
self.realized_profit = 0
self.recalc_open_trade_value()
if not from_json:
# Skip recalculation when loading from json
self.realized_profit = 0
self.recalc_open_trade_value()
@validates('enter_tag', 'exit_reason')
def validate_string_len(self, key, value):
@ -1655,6 +1669,7 @@ class Trade(ModelBase, LocalTrade):
import rapidjson
data = rapidjson.loads(json_str)
trade = cls(
__FROM_JSON=True,
id=data["trade_id"],
pair=data["pair"],
base_currency=data["base_currency"],

View File

@ -84,7 +84,7 @@ def init_plotscript(config, markets: List, startup_candles: int = 0):
except ValueError as e:
raise OperationalException(e) from e
if not trades.empty:
trades = trim_dataframe(trades, timerange, 'open_date')
trades = trim_dataframe(trades, timerange, df_date_col='open_date')
return {"ohlcv": data,
"trades": trades,

View File

@ -3,15 +3,16 @@ Remote PairList provider
Provides pair list fetched from a remote source
"""
import json
import logging
from pathlib import Path
from typing import Any, Dict, List, Tuple
import rapidjson
import requests
from cachetools import TTLCache
from freqtrade import __version__
from freqtrade.configuration.load_config import CONFIG_PARSE_MODE
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers
@ -236,7 +237,7 @@ class RemotePairList(IPairList):
if file_path.exists():
with file_path.open() as json_file:
# Load the JSON data into a dictionary
jsonparse = json.load(json_file)
jsonparse = rapidjson.load(json_file, parse_mode=CONFIG_PARSE_MODE)
try:
pairlist = self.process_json(jsonparse)

View File

@ -2,6 +2,7 @@ import asyncio
import logging
from copy import deepcopy
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List
from fastapi import APIRouter, BackgroundTasks, Depends
@ -9,11 +10,12 @@ from fastapi.exceptions import HTTPException
from freqtrade.configuration.config_validation import validate_config_consistency
from freqtrade.constants import Config
from freqtrade.data.btanalysis import get_backtest_resultlist, load_and_merge_backtest_result
from freqtrade.data.btanalysis import (delete_backtest_result, get_backtest_resultlist,
load_and_merge_backtest_result)
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
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.deps import get_config
@ -245,13 +247,16 @@ def api_backtest_history(config=Depends(get_config)):
tags=['webserver', 'backtest'])
def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config)):
# Get backtest result history, read from metadata files
fn = config['user_data_dir'] / 'backtest_results' / filename
bt_results_base: Path = config['user_data_dir'] / 'backtest_results'
fn = (bt_results_base / filename).with_suffix('.json')
results: Dict[str, Any] = {
'metadata': {},
'strategy': {},
'strategy_comparison': [],
}
if not is_file_in_dir(fn, bt_results_base):
raise HTTPException(status_code=404, detail="File not found.")
load_and_merge_backtest_result(strategy, fn, results)
return {
"status": "ended",
@ -261,3 +266,17 @@ def api_backtest_history_result(filename: str, strategy: str, config=Depends(get
"status_msg": "Historic result",
"backtest_result": results,
}
@router.delete('/backtest/history/{file}', response_model=List[BacktestHistoryEntry],
tags=['webserver', 'backtest'])
def api_delete_backtest_history_entry(file: str, 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.")
delete_backtest_result(file_abs)
return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results')

View File

@ -136,6 +136,9 @@ class Profit(BaseModel):
winning_trades: int
losing_trades: int
profit_factor: float
winrate: float
expectancy: float
expectancy_ratio: float
max_drawdown: float
max_drawdown_abs: float
trading_volume: Optional[float]

View File

@ -49,7 +49,8 @@ logger = logging.getLogger(__name__)
# 2.28: Switch reload endpoint to Post
# 2.29: Add /exchanges endpoint
# 2.30: new /pairlists endpoint
API_VERSION = 2.30
# 2.31: new /backtest/history/ delete endpoint
API_VERSION = 2.31
# Public API, requires no auth.
router_public = APIRouter()
@ -268,7 +269,10 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str,
'timerange': timerange,
'freqaimodel': freqaimodel if freqaimodel else config.get('freqaimodel'),
})
return RPC._rpc_analysed_history_full(config, pair, timeframe, exchange)
try:
return RPC._rpc_analysed_history_full(config, pair, timeframe, exchange)
except Exception as e:
raise HTTPException(status_code=502, detail=str(e))
@router.get('/plot_config', response_model=PlotConfig, tags=['candle data'])
@ -283,7 +287,10 @@ def plot_config(strategy: Optional[str] = None, config=Depends(get_config),
config1.update({
'strategy': strategy
})
try:
return PlotConfig.parse_obj(RPC._rpc_plot_config_with_strategy(config1))
except Exception as e:
raise HTTPException(status_code=502, detail=str(e))
@router.get('/strategies', response_model=StrategyListResponse, tags=['strategy'])
@ -308,7 +315,8 @@ def get_strategy(strategy: str, config=Depends(get_config)):
extra_dir=config_.get('strategy_path'))
except OperationalException:
raise HTTPException(status_code=404, detail='Strategy not found')
except Exception as e:
raise HTTPException(status_code=502, detail=str(e))
return {
'strategy': strategy_obj.get_strategy_name(),
'code': strategy_obj.__source__,

View File

@ -30,7 +30,7 @@ async def ui_version():
}
def is_relative_to(path, base) -> bool:
def is_relative_to(path: Path, base: Path) -> bool:
# Helper function simulating behaviour of is_relative_to, which was only added in python 3.9
try:
path.relative_to(base)

View File

@ -25,6 +25,7 @@ coingecko_mapping = {
'bnb': 'binancecoin',
'sol': 'solana',
'usdt': 'tether',
'busd': 'binance-usd',
}

View File

@ -18,7 +18,7 @@ from freqtrade import __version__
from freqtrade.configuration.timerange import TimeRange
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT, Config
from freqtrade.data.history import load_data
from freqtrade.data.metrics import calculate_max_drawdown
from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, SignalDirection,
State, TradingMode)
from freqtrade.exceptions import ExchangeError, PricingError
@ -494,6 +494,8 @@ class RPC:
profit_all_coin.append(profit_abs)
profit_all_ratio.append(profit_ratio)
closed_trade_count = len([t for t in trades if not t.is_open])
best_pair = Trade.get_best_pair(start_date)
trading_volume = Trade.get_trading_volume(start_date)
@ -521,9 +523,14 @@ class RPC:
profit_factor = winning_profit / abs(losing_profit) if losing_profit else float('inf')
winrate = (winning_trades / closed_trade_count) if closed_trade_count > 0 else 0
trades_df = DataFrame([{'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT),
'profit_abs': trade.close_profit_abs}
for trade in trades if not trade.is_open and trade.close_date])
expectancy, expectancy_ratio = calculate_expectancy(trades_df)
max_drawdown_abs = 0.0
max_drawdown = 0.0
if len(trades_df) > 0:
@ -562,7 +569,7 @@ class RPC:
'profit_all_percent': round(profit_all_ratio_fromstart * 100, 2),
'profit_all_fiat': profit_all_fiat,
'trade_count': len(trades),
'closed_trade_count': len([t for t in trades if not t.is_open]),
'closed_trade_count': closed_trade_count,
'first_trade_date': first_date.strftime(DATETIME_PRINT_FORMAT) if first_date else '',
'first_trade_humanized': dt_humanize(first_date) if first_date else '',
'first_trade_timestamp': int(first_date.timestamp() * 1000) if first_date else 0,
@ -576,6 +583,9 @@ class RPC:
'winning_trades': winning_trades,
'losing_trades': losing_trades,
'profit_factor': profit_factor,
'winrate': winrate,
'expectancy': expectancy,
'expectancy_ratio': expectancy_ratio,
'max_drawdown': max_drawdown,
'max_drawdown_abs': max_drawdown_abs,
'trading_volume': trading_volume,
@ -1169,8 +1179,8 @@ class RPC:
""" Analyzed dataframe in Dict form """
_data, last_analyzed = self.__rpc_analysed_dataframe_raw(pair, timeframe, limit)
return self._convert_dataframe_to_dict(self._freqtrade.config['strategy'],
pair, timeframe, _data, last_analyzed)
return RPC._convert_dataframe_to_dict(self._freqtrade.config['strategy'],
pair, timeframe, _data, last_analyzed)
def __rpc_analysed_dataframe_raw(
self,
@ -1240,27 +1250,34 @@ class RPC:
exchange) -> Dict[str, Any]:
timerange_parsed = TimeRange.parse_timerange(config.get('timerange'))
from freqtrade.data.converter import trim_dataframe
from freqtrade.data.dataprovider import DataProvider
from freqtrade.resolvers.strategy_resolver import StrategyResolver
strategy = StrategyResolver.load_strategy(config)
startup_candles = strategy.startup_candle_count
_data = load_data(
datadir=config["datadir"],
pairs=[pair],
timeframe=timeframe,
timerange=timerange_parsed,
data_format=config['dataformat_ohlcv'],
candle_type=config.get('candle_type_def', CandleType.SPOT)
candle_type=config.get('candle_type_def', CandleType.SPOT),
startup_candles=startup_candles,
)
if pair not in _data:
raise RPCException(
f"No data for {pair}, {timeframe} in {config.get('timerange')} found.")
from freqtrade.data.dataprovider import DataProvider
from freqtrade.resolvers.strategy_resolver import StrategyResolver
strategy = StrategyResolver.load_strategy(config)
strategy.dp = DataProvider(config, exchange=exchange, pairlists=None)
strategy.ft_bot_start()
df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair})
df_analyzed = trim_dataframe(df_analyzed, timerange_parsed, startup_candles=startup_candles)
return RPC._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe,
df_analyzed, dt_now())
df_analyzed.copy(), dt_now())
def _rpc_plot_config(self) -> Dict[str, Any]:
if (self._freqtrade.strategy.plot_config and

View File

@ -849,6 +849,10 @@ class Telegram(RPCHandler):
avg_duration = stats['avg_duration']
best_pair = stats['best_pair']
best_pair_profit_ratio = stats['best_pair_profit_ratio']
winrate = stats['winrate']
expectancy = stats['expectancy']
expectancy_ratio = stats['expectancy_ratio']
if stats['trade_count'] == 0:
markdown_msg = f"No trades yet.\n*Bot started:* `{stats['bot_start_date']}`"
else:
@ -873,7 +877,9 @@ class Telegram(RPCHandler):
f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* "
f"`{first_trade_date}`\n"
f"*Latest Trade opened:* `{latest_trade_date}`\n"
f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`"
f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`\n"
f"*Winrate:* `{winrate:.2%}`\n"
f"*Expectancy (Ratio):* `{expectancy:.2f} ({expectancy_ratio:.2f})`"
)
if stats['closed_trade_count'] > 0:
markdown_msg += (

View File

@ -34,6 +34,7 @@ class Webhook(RPCHandler):
self._format = self._config['webhook'].get('format', 'form')
self._retries = self._config['webhook'].get('retries', 0)
self._retry_delay = self._config['webhook'].get('retry_delay', 0.1)
self._timeout = self._config['webhook'].get('timeout', 10)
def cleanup(self) -> None:
"""
@ -107,12 +108,13 @@ class Webhook(RPCHandler):
try:
if self._format == 'form':
response = post(self._url, data=payload)
response = post(self._url, data=payload, timeout=self._timeout)
elif self._format == 'json':
response = post(self._url, json=payload)
response = post(self._url, json=payload, timeout=self._timeout)
elif self._format == 'raw':
response = post(self._url, data=payload['data'],
headers={'Content-Type': 'text/plain'})
headers={'Content-Type': 'text/plain'},
timeout=self._timeout)
else:
raise NotImplementedError(f'Unknown format: {self._format}')

View File

@ -825,6 +825,7 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
Parses the given candle (OHLCV) data and returns a populated DataFrame
add several TA indicators and entry order signal to it
Should only be used in live.
:param dataframe: Dataframe containing data from exchange
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
:return: DataFrame of candle (OHLCV) data with indicator data and signals added
@ -1321,6 +1322,20 @@ class IStrategy(ABC, HyperStrategyMixin):
return {pair: self.advise_indicators(pair_data.copy(), {'pair': pair}).copy()
for pair, pair_data in data.items()}
def ft_advise_signals(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Call advise_entry and advise_exit and return the resulting dataframe.
:param dataframe: Dataframe containing data from exchange, as well as pre-calculated
indicators
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
:return: DataFrame of candle (OHLCV) data with indicator data and signals added
"""
dataframe = self.advise_entry(dataframe, metadata)
dataframe = self.advise_exit(dataframe, metadata)
return dataframe
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Populate indicators that will be used in the Buy, Sell, short, exit_short strategy

View File

@ -84,6 +84,7 @@ class Wallets:
tot_profit = Trade.get_total_closed_profit()
else:
tot_profit = LocalTrade.total_profit
tot_profit += sum(trade.realized_profit for trade in open_trades)
tot_in_trades = sum(trade.stake_amount for trade in open_trades)
used_stake = 0.0

View File

@ -7,11 +7,11 @@
-r docs/requirements-docs.txt
coveralls==3.3.1
ruff==0.0.277
ruff==0.0.280
mypy==1.4.1
pre-commit==3.3.3
pytest==7.4.0
pytest-asyncio==0.21.0
pytest-asyncio==0.21.1
pytest-cov==4.1.0
pytest-mock==3.11.1
pytest-random-order==1.1.0
@ -20,11 +20,11 @@ isort==5.12.0
time-machine==2.11.0
# Convert jupyter notebooks to markdown documents
nbconvert==7.6.0
nbconvert==7.7.2
# mypy types
types-cachetools==5.3.0.5
types-cachetools==5.3.0.6
types-filelock==3.2.7
types-requests==2.31.0.1
types-tabulate==0.9.0.2
types-python-dateutil==2.8.19.13
types-requests==2.31.0.2
types-tabulate==0.9.0.3
types-python-dateutil==2.8.19.14

View File

@ -6,7 +6,7 @@
scikit-learn==1.1.3
joblib==1.3.1
catboost==1.2; 'arm' not in platform_machine
lightgbm==3.3.5
lightgbm==4.0.0
xgboost==1.7.6
tensorboard==2.13.0
datasieve==0.1.7

View File

@ -3,20 +3,20 @@ numpy==1.24.3; python_version <= '3.8'
pandas==2.0.3
pandas-ta==0.3.14b
ccxt==4.0.17
cryptography==41.0.1; platform_machine != 'armv7l'
ccxt==4.0.36
cryptography==41.0.2; platform_machine != 'armv7l'
cryptography==40.0.1; platform_machine == 'armv7l'
aiohttp==3.8.4
SQLAlchemy==2.0.18
aiohttp==3.8.5
SQLAlchemy==2.0.19
python-telegram-bot==20.4
# can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1
arrow==1.2.3
cachetools==5.3.1
requests==2.31.0
urllib3==2.0.3
jsonschema==4.18.0
TA-Lib==0.4.26
urllib3==2.0.4
jsonschema==4.18.4
TA-Lib==0.4.27
technical==1.4.0
tabulate==0.9.0
pycoingecko==3.1.0
@ -25,7 +25,7 @@ tables==3.8.0
blosc==1.11.1
joblib==1.3.1
rich==13.4.2
pyarrow==12.0.0; platform_machine != 'armv7l'
pyarrow==12.0.1; platform_machine != 'armv7l'
# find first, C search in arrays
py_find_1st==1.1.5
@ -40,9 +40,9 @@ sdnotify==0.3.2
# API Server
fastapi==0.100.0
pydantic==1.10.9
uvicorn==0.22.0
pyjwt==2.7.0
pydantic==1.10.11
uvicorn==0.23.1
pyjwt==2.8.0
aiofiles==23.1.0
psutil==5.9.5

View File

@ -97,7 +97,7 @@ setup(
'rich',
'pyarrow; platform_machine != "armv7l"',
'fastapi',
'pydantic>=1.8.0',
'pydantic>=1.8.0,<2.0',
'uvicorn',
'psutil',
'pyjwt',

View File

@ -3002,85 +3002,85 @@ def mark_ohlcv():
def funding_rate_history_hourly():
return [
{
"symbol": "ADA/USDT",
"symbol": "ADA/USDT:USDT",
"fundingRate": -0.000008,
"timestamp": 1630454400000,
"datetime": "2021-09-01T00:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"symbol": "ADA/USDT:USDT",
"fundingRate": -0.000004,
"timestamp": 1630458000000,
"datetime": "2021-09-01T01:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"symbol": "ADA/USDT:USDT",
"fundingRate": 0.000012,
"timestamp": 1630461600000,
"datetime": "2021-09-01T02:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"symbol": "ADA/USDT:USDT",
"fundingRate": -0.000003,
"timestamp": 1630465200000,
"datetime": "2021-09-01T03:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"symbol": "ADA/USDT:USDT",
"fundingRate": -0.000007,
"timestamp": 1630468800000,
"datetime": "2021-09-01T04:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"symbol": "ADA/USDT:USDT",
"fundingRate": 0.000003,
"timestamp": 1630472400000,
"datetime": "2021-09-01T05:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"symbol": "ADA/USDT:USDT",
"fundingRate": 0.000019,
"timestamp": 1630476000000,
"datetime": "2021-09-01T06:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"symbol": "ADA/USDT:USDT",
"fundingRate": 0.000003,
"timestamp": 1630479600000,
"datetime": "2021-09-01T07:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"symbol": "ADA/USDT:USDT",
"fundingRate": -0.000003,
"timestamp": 1630483200000,
"datetime": "2021-09-01T08:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"symbol": "ADA/USDT:USDT",
"fundingRate": 0,
"timestamp": 1630486800000,
"datetime": "2021-09-01T09:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"symbol": "ADA/USDT:USDT",
"fundingRate": 0.000013,
"timestamp": 1630490400000,
"datetime": "2021-09-01T10:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"symbol": "ADA/USDT:USDT",
"fundingRate": 0.000077,
"timestamp": 1630494000000,
"datetime": "2021-09-01T11:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"symbol": "ADA/USDT:USDT",
"fundingRate": 0.000072,
"timestamp": 1630497600000,
"datetime": "2021-09-01T12:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"symbol": "ADA/USDT:USDT",
"fundingRate": 0.000097,
"timestamp": 1630501200000,
"datetime": "2021-09-01T13:00:00.000Z"
@ -3092,13 +3092,13 @@ def funding_rate_history_hourly():
def funding_rate_history_octohourly():
return [
{
"symbol": "ADA/USDT",
"symbol": "ADA/USDT:USDT",
"fundingRate": -0.000008,
"timestamp": 1630454400000,
"datetime": "2021-09-01T00:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"symbol": "ADA/USDT:USDT",
"fundingRate": -0.000003,
"timestamp": 1630483200000,
"datetime": "2021-09-01T08:00:00.000Z"

View File

@ -343,12 +343,24 @@ def test_calculate_expectancy(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
expectancy = calculate_expectancy(DataFrame())
expectancy, expectancy_ratio = calculate_expectancy(DataFrame())
assert expectancy == 0.0
assert expectancy_ratio == 100
expectancy = calculate_expectancy(bt_data)
expectancy, expectancy_ratio = calculate_expectancy(bt_data)
assert isinstance(expectancy, float)
assert pytest.approx(expectancy) == 0.07151374226574791
assert isinstance(expectancy_ratio, float)
assert pytest.approx(expectancy) == 5.820687070932315e-06
assert pytest.approx(expectancy_ratio) == 0.07151374226574791
data = {
'profit_abs': [100, 200, 50, -150, 300, -100, 80, -30]
}
df = DataFrame(data)
expectancy, expectancy_ratio = calculate_expectancy(df)
assert pytest.approx(expectancy) == 56.25
assert pytest.approx(expectancy_ratio) == 0.60267857
def test_calculate_sortino(testdatadir):

View File

@ -135,6 +135,73 @@ def test_ohlcv_fill_up_missing_data2(caplog):
f"{len(data)} - after: {len(data2)}.*", caplog)
def test_ohlcv_to_dataframe_1M():
# Monthly ticks from 2019-09-01 to 2023-07-01
ticks = [
[1567296000000, 8042.08, 10475.54, 7700.67, 8041.96, 608742.1109999999],
[1569888000000, 8285.31, 10408.48, 7172.76, 9150.0, 2439561.887],
[1572566400000, 9149.88, 9550.0, 6510.19, 7542.93, 4042674.725],
[1575158400000, 7541.08, 7800.0, 6427.0, 7189.0, 4063882.296],
[1577836800000, 7189.43, 9599.0, 6863.44, 9364.51, 5165281.358],
[1580515200000, 9364.5, 10540.0, 8450.0, 8531.98, 4581788.124],
[1583020800000, 8532.5, 9204.0, 3621.81, 6407.1, 10859497.479],
[1585699200000, 6407.1, 9479.77, 6140.0, 8624.76, 11276526.968],
[1588291200000, 8623.61, 10080.0, 7940.0, 9446.43, 12469561.02],
[1590969600000, 9446.49, 10497.25, 8816.4, 9138.87, 6684044.201],
[1593561600000, 9138.88, 11488.0, 8900.0, 11343.68, 5709327.926],
[1596240000000, 11343.67, 12499.42, 10490.0, 11658.11, 6746487.129],
[1598918400000, 11658.11, 12061.07, 9808.58, 10773.0, 6442697.051],
[1601510400000, 10773.0, 14140.0, 10371.03, 13783.73, 7404103.004],
[1604188800000, 13783.73, 19944.0, 13195.0, 19720.0, 12328272.549],
[1606780800000, 19722.09, 29376.7, 17555.0, 28951.68, 10067314.24],
[1609459200000, 28948.19, 42125.51, 27800.0, 33126.21, 12408873.079],
[1612137600000, 33125.11, 58472.14, 32322.47, 45163.36, 8784474.482],
[1614556800000, 45162.64, 61950.0, 44972.49, 58807.24, 9459821.267],
[1617235200000, 58810.99, 64986.11, 46930.43, 57684.16, 7895051.389],
[1619827200000, 57688.29, 59654.0, 28688.0, 37243.38, 16790964.443],
[1622505600000, 37244.36, 41413.0, 28780.01, 35031.39, 23474519.886],
[1625097600000, 35031.39, 48168.6, 29242.24, 41448.11, 16932491.175],
[1627776000000, 41448.1, 50600.0, 37291.0, 47150.32, 13645800.254],
[1630454400000, 47150.32, 52950.0, 39503.58, 43796.57, 10734742.869],
[1633046400000, 43799.49, 67150.0, 43260.01, 61348.61, 9111112.847],
[1635724800000, 61347.14, 69198.7, 53245.0, 56975.0, 7111424.463],
[1638316800000, 56978.06, 59100.0, 40888.89, 46210.56, 8404449.024],
[1640995200000, 46210.57, 48000.0, 32853.83, 38439.04, 11047479.277],
[1643673600000, 38439.04, 45847.5, 34303.7, 43155.0, 10910339.91],
[1646092800000, 43155.0, 48200.0, 37134.0, 45506.0, 10459721.586],
[1648771200000, 45505.9, 47448.0, 37550.0, 37614.5, 8463568.862],
[1651363200000, 37614.4, 40071.7, 26631.0, 31797.8, 14463715.774],
[1654041600000, 31797.9, 31986.1, 17593.2, 19923.5, 20710810.306],
[1656633600000, 19923.3, 24700.0, 18780.1, 23290.1, 20582518.513],
[1659312000000, 23290.1, 25200.0, 19508.0, 20041.5, 17221921.557],
[1661990400000, 20041.4, 22850.0, 18084.3, 19411.7, 21935261.414],
[1664582400000, 19411.6, 21088.0, 17917.8, 20482.0, 16625843.584],
[1667260800000, 20482.1, 21473.7, 15443.2, 17153.3, 18460614.013],
[1669852800000, 17153.4, 18400.0, 16210.0, 16537.6, 9702408.711],
[1672531200000, 16537.5, 23962.7, 16488.0, 23119.4, 14732180.645],
[1675209600000, 23119.5, 25347.6, 21338.0, 23129.6, 15025197.415],
[1677628800000, 23129.7, 29184.8, 19521.6, 28454.9, 23317458.541],
[1680307200000, 28454.8, 31059.0, 26919.3, 29223.0, 14654208.219],
[1682899200000, 29223.0, 29840.0, 25751.0, 27201.1, 13328157.284],
[1685577600000, 27201.1, 31500.0, 24777.0, 30460.2, 14099299.273],
[1688169600000, 30460.2, 31850.0, 28830.0, 29338.8, 8760361.377]
]
data = ohlcv_to_dataframe(ticks, '1M', pair="UNITTEST/USDT",
fill_missing=False, drop_incomplete=False)
assert len(data) == len(ticks)
assert data.iloc[0]['date'].strftime('%Y-%m-%d') == '2019-09-01'
assert data.iloc[-1]['date'].strftime('%Y-%m-%d') == '2023-07-01'
# Test with filling missing data
data = ohlcv_to_dataframe(ticks, '1M', pair="UNITTEST/USDT",
fill_missing=True, drop_incomplete=False)
assert len(data) == len(ticks)
assert data.iloc[0]['date'].strftime('%Y-%m-%d') == '2019-09-01'
assert data.iloc[-1]['date'].strftime('%Y-%m-%d') == '2023-07-01'
def test_ohlcv_drop_incomplete(caplog):
timeframe = '1d'
ticks = [

View File

@ -544,6 +544,8 @@ 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")

View File

@ -4343,11 +4343,11 @@ def test__fetch_and_calculate_funding_fees(
ex = get_patched_exchange(mocker, default_conf, api_mock, id=exchange)
mocker.patch(f'{EXMS}.timeframes', PropertyMock(return_value=['1h', '4h', '8h']))
funding_fees = ex._fetch_and_calculate_funding_fees(
pair='ADA/USDT', amount=amount, is_short=True, open_date=d1, close_date=d2)
pair='ADA/USDT:USDT', amount=amount, is_short=True, open_date=d1, close_date=d2)
assert pytest.approx(funding_fees) == expected_fees
# Fees for Longs are inverted
funding_fees = ex._fetch_and_calculate_funding_fees(
pair='ADA/USDT', amount=amount, is_short=False, open_date=d1, close_date=d2)
pair='ADA/USDT:USDT', amount=amount, is_short=False, open_date=d1, close_date=d2)
assert pytest.approx(funding_fees) == -expected_fees
# Return empty "refresh_latest"
@ -4355,7 +4355,7 @@ def test__fetch_and_calculate_funding_fees(
ex = get_patched_exchange(mocker, default_conf, api_mock, id=exchange)
with pytest.raises(ExchangeError, match="Could not find funding rates."):
ex._fetch_and_calculate_funding_fees(
pair='ADA/USDT', amount=amount, is_short=False, open_date=d1, close_date=d2)
pair='ADA/USDT:USDT', amount=amount, is_short=False, open_date=d1, close_date=d2)
@pytest.mark.parametrize('exchange,expected_fees', [
@ -5424,7 +5424,7 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun
assert api_mock.create_order.call_args_list[0][1]['amount'] == order_amount
assert order['amount'] == 100
assert order['cost'] == 100
assert order['cost'] == order_amount
assert order['filled'] == 100
assert order['remaining'] == 100

View File

@ -601,6 +601,9 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
trade = backtesting._enter_trade(pair, row=row, direction='short')
assert pytest.approx(trade.liquidation_price) == 0.11787191
assert pytest.approx(trade.orders[0].cost) == (
trade.stake_amount * trade.leverage + trade.fee_open)
assert pytest.approx(trade.orders[-1].stake_amount) == trade.stake_amount
# Stake-amount too high!
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=600.0)

View File

@ -23,7 +23,8 @@ from freqtrade.optimize.optimize_reports import (generate_backtest_stats, genera
store_backtest_analysis_results,
store_backtest_stats, text_table_bt_results,
text_table_exit_reason, text_table_strategy)
from freqtrade.optimize.optimize_reports.optimize_reports import _get_resample_from_period
from freqtrade.optimize.optimize_reports.optimize_reports import (_get_resample_from_period,
calc_streak)
from freqtrade.resolvers.strategy_resolver import StrategyResolver
from freqtrade.util import dt_ts
from freqtrade.util.datetime_helpers import dt_from_ts, dt_utc
@ -348,6 +349,32 @@ def test_generate_trading_stats(testdatadir):
assert res['losses'] == 0
def test_calc_streak(testdatadir):
df = pd.DataFrame({
'profit_ratio': [0.05, -0.02, -0.03, -0.05, 0.01, 0.02, 0.03, 0.04, -0.02, -0.03],
})
# 4 consecutive wins, 3 consecutive losses
res = calc_streak(df)
assert res == (4, 3)
assert isinstance(res[0], int)
assert isinstance(res[1], int)
# invert situation
df1 = df.copy()
df1['profit_ratio'] = df1['profit_ratio'] * -1
assert calc_streak(df1) == (3, 4)
df_empty = pd.DataFrame({
'profit_ratio': [],
})
assert df_empty.empty
assert calc_streak(df_empty) == (0, 0)
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
assert calc_streak(bt_data) == (7, 18)
def test_text_table_exit_reason():
results = pd.DataFrame(

View File

@ -563,14 +563,14 @@ def test_calc_open_close_trade_price(
trade.open_order_id = f'something-{is_short}-{lev}-{exchange}'
oobj = Order.parse_from_ccxt_object(entry_order, 'ADA/USDT', trade.entry_side)
oobj.trade = trade
oobj._trade_live = trade
oobj.update_from_ccxt_object(entry_order)
trade.update_trade(oobj)
trade.funding_fees = funding_fees
oobj = Order.parse_from_ccxt_object(exit_order, 'ADA/USDT', trade.exit_side)
oobj.trade = trade
oobj._trade_live = trade
oobj.update_from_ccxt_object(exit_order)
trade.update_trade(oobj)

View File

@ -179,6 +179,7 @@ def test_trade_fromjson():
assert trade.open_date_utc == datetime(2022, 10, 18, 9, 12, 42, tzinfo=timezone.utc)
assert isinstance(trade.open_date, datetime)
assert trade.exit_reason == 'no longer good'
assert trade.realized_profit == 2.76315361
assert len(trade.orders) == 5
last_o = trade.orders[-1]

View File

@ -35,7 +35,7 @@ def test_gen_pairlist_with_local_file(mocker, rpl_config):
mock_file_path.exists.return_value = True
jsonparse = json.loads(mock_file.read.return_value)
mocker.patch('freqtrade.plugins.pairlist.RemotePairList.json.load', return_value=jsonparse)
mocker.patch('freqtrade.plugins.pairlist.RemotePairList.rapidjson.load', return_value=jsonparse)
rpl_config['pairlists'] = [
{

View File

@ -402,6 +402,8 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
assert res['first_trade_timestamp'] == 0
assert res['latest_trade_date'] == ''
assert res['latest_trade_timestamp'] == 0
assert res['expectancy'] == 0
assert res['expectancy_ratio'] == 100
# Create some test data
create_mock_trades_usdt(fee)
@ -413,6 +415,9 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
assert pytest.approx(stats['profit_all_coin']) == -77.45964918
assert pytest.approx(stats['profit_all_percent_mean']) == -57.86
assert pytest.approx(stats['profit_all_fiat']) == -85.205614098
assert pytest.approx(stats['winrate']) == 0.666666667
assert pytest.approx(stats['expectancy']) == 0.913333333
assert pytest.approx(stats['expectancy_ratio']) == 0.223308883
assert stats['trade_count'] == 7
assert stats['first_trade_humanized'] == '2 days ago'
assert stats['latest_trade_humanized'] == '17 minutes ago'

View File

@ -829,7 +829,8 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
'profit_closed_percent_mean': -0.75, 'profit_closed_ratio_sum': -0.015,
'profit_closed_percent_sum': -1.5, 'profit_closed_ratio': -6.739057628404269e-06,
'profit_closed_percent': -0.0, 'winning_trades': 0, 'losing_trades': 2,
'profit_factor': 0.0, 'trading_volume': 91.074,
'profit_factor': 0.0, 'winrate': 0.0, 'expectancy': -0.0033695635,
'expectancy_ratio': -1.0, 'trading_volume': 91.074,
}
),
(
@ -844,7 +845,8 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
'profit_closed_percent_mean': 0.75, 'profit_closed_ratio_sum': 0.015,
'profit_closed_percent_sum': 1.5, 'profit_closed_ratio': 7.391275897987988e-07,
'profit_closed_percent': 0.0, 'winning_trades': 2, 'losing_trades': 0,
'profit_factor': None, 'trading_volume': 91.074,
'profit_factor': None, 'winrate': 1.0, 'expectancy': 0.0003695635,
'expectancy_ratio': 100, 'trading_volume': 91.074,
}
),
(
@ -859,7 +861,9 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
'profit_closed_percent_mean': 0.25, 'profit_closed_ratio_sum': 0.005,
'profit_closed_percent_sum': 0.5, 'profit_closed_ratio': -5.429078808526421e-06,
'profit_closed_percent': -0.0, 'winning_trades': 1, 'losing_trades': 1,
'profit_factor': 0.02775724835771106, 'trading_volume': 91.074,
'profit_factor': 0.02775724835771106, 'winrate': 0.5,
'expectancy': -0.0027145635000000003, 'expectancy_ratio': -0.48612137582114445,
'trading_volume': 91.074,
}
)
])
@ -916,6 +920,9 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected)
'winning_trades': expected['winning_trades'],
'losing_trades': expected['losing_trades'],
'profit_factor': expected['profit_factor'],
'winrate': expected['winrate'],
'expectancy': expected['expectancy'],
'expectancy_ratio': expected['expectancy_ratio'],
'max_drawdown': ANY,
'max_drawdown_abs': ANY,
'trading_volume': expected['trading_volume'],
@ -1469,30 +1476,47 @@ def test_api_pair_history(botclient, mocker):
"&timerange=20180111-20180112")
assert_response(rc, 422)
# Invalid strategy
rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}11")
assert_response(rc, 502)
# Working
rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}")
assert_response(rc, 200)
assert rc.json()['length'] == 289
assert len(rc.json()['data']) == rc.json()['length']
assert 'columns' in rc.json()
assert 'data' in rc.json()
result = rc.json()
assert result['length'] == 289
assert len(result['data']) == result['length']
assert 'columns' in result
assert 'data' in result
data = result['data']
assert len(data) == 289
# analyed DF has 28 columns
assert len(result['columns']) == 28
assert len(data[0]) == 28
date_col_idx = [idx for idx, c in enumerate(result['columns']) if c == 'date'][0]
rsi_col_idx = [idx for idx, c in enumerate(result['columns']) if c == 'rsi'][0]
assert data[0][date_col_idx] == '2018-01-11 00:00:00'
assert data[0][rsi_col_idx] is not None
assert data[0][rsi_col_idx] > 0
assert lfm.call_count == 1
assert rc.json()['pair'] == 'UNITTEST/BTC'
assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY
assert rc.json()['data_start'] == '2018-01-11 00:00:00+00:00'
assert rc.json()['data_start_ts'] == 1515628800000
assert rc.json()['data_stop'] == '2018-01-12 00:00:00+00:00'
assert rc.json()['data_stop_ts'] == 1515715200000
assert result['pair'] == 'UNITTEST/BTC'
assert result['strategy'] == CURRENT_TEST_STRATEGY
assert result['data_start'] == '2018-01-11 00:00:00+00:00'
assert result['data_start_ts'] == 1515628800000
assert result['data_stop'] == '2018-01-12 00:00:00+00:00'
assert result['data_stop_ts'] == 1515715200000
# No data found
rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
f"&timerange=20200111-20200112&strategy={CURRENT_TEST_STRATEGY}")
assert_response(rc, 502)
assert rc.json()['error'] == ("Error querying /api/v1/pair_history: "
"No data for UNITTEST/BTC, 5m in 20200111-20200112 found.")
assert rc.json()['detail'] == ("No data for UNITTEST/BTC, 5m in 20200111-20200112 found.")
def test_api_plot_config(botclient, mocker):
@ -1529,6 +1553,10 @@ def test_api_plot_config(botclient, mocker):
assert_response(rc)
assert rc.json()['subplots'] == {}
rc = client_get(client, f"{BASE_URI}/plot_config?strategy=NotAStrategy")
assert_response(rc, 502)
assert rc.json()['detail'] is not None
mocker.patch('freqtrade.rpc.api_server.api_v1.get_rpc_optional', return_value=None)
rc = client_get(client, f"{BASE_URI}/plot_config")
@ -1981,7 +2009,7 @@ def test_api_backtest_history(botclient, mocker, testdatadir):
result = rc.json()
assert len(result) == 3
fn = result[0]['filename']
assert fn == "backtest-result_multistrat.json"
assert fn == "backtest-result_multistrat"
strategy = result[0]['strategy']
rc = client_get(client, f"{BASE_URI}/backtest/history/result?filename={fn}&strategy={strategy}")
assert_response(rc)
@ -1995,6 +2023,34 @@ 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):
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')
meta_path.touch()
rc = client_delete(client, f"{BASE_URI}/backtest/history/randomFile.json")
assert_response(rc, 503)
assert rc.json()['detail'] == 'Bot is not in the correct state.'
ftbot.config['user_data_dir'] = tmp_path
ftbot.config['runmode'] = RunMode.WEBSERVER
rc = client_delete(client, f"{BASE_URI}/backtest/history/randomFile.json")
assert rc.status_code == 404
assert rc.json()['detail'] == 'File not found.'
rc = client_delete(client, f"{BASE_URI}/backtest/history/{file_path.name}")
assert rc.status_code == 200
assert not file_path.exists()
assert not meta_path.exists()
def test_health(botclient):
ftbot, client = botclient

View File

@ -799,6 +799,8 @@ async def test_telegram_profit_handle(
assert '*Best Performing:* `ETH/USDT: 9.45%`' in msg_mock.call_args_list[-1][0][0]
assert '*Max Drawdown:*' in msg_mock.call_args_list[-1][0][0]
assert '*Profit factor:*' in msg_mock.call_args_list[-1][0][0]
assert '*Winrate:*' in msg_mock.call_args_list[-1][0][0]
assert '*Expectancy (Ratio):*' in msg_mock.call_args_list[-1][0][0]
assert '*Trading volume:* `126 USDT`' in msg_mock.call_args_list[-1][0][0]

View File

@ -381,7 +381,7 @@ def test__send_msg(default_conf, mocker, caplog):
webhook._send_msg(msg)
assert post.call_count == 1
assert post.call_args[1] == {'data': msg}
assert post.call_args[1] == {'data': msg, 'timeout': 10}
assert post.call_args[0] == (default_conf['webhook']['url'], )
post = MagicMock(side_effect=RequestException)
@ -399,7 +399,7 @@ def test__send_msg_with_json_format(default_conf, mocker, caplog):
mocker.patch("freqtrade.rpc.webhook.post", post)
webhook._send_msg(msg)
assert post.call_args[1] == {'json': msg}
assert post.call_args[1] == {'json': msg, 'timeout': 10}
def test__send_msg_with_raw_format(default_conf, mocker, caplog):
@ -411,7 +411,11 @@ def test__send_msg_with_raw_format(default_conf, mocker, caplog):
mocker.patch("freqtrade.rpc.webhook.post", post)
webhook._send_msg(msg)
assert post.call_args[1] == {'data': msg['data'], 'headers': {'Content-Type': 'text/plain'}}
assert post.call_args[1] == {
'data': msg['data'],
'headers': {'Content-Type': 'text/plain'},
'timeout': 10
}
def test_send_msg_discord(default_conf, mocker):

View File

@ -2793,7 +2793,7 @@ def test_manage_open_orders_entry(
freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False)
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1234)
# check it does cancel buy orders over the time limit
# check it does cancel entry orders over the time limit
freqtrade.manage_open_orders()
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 2
@ -2801,7 +2801,7 @@ def test_manage_open_orders_entry(
select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
nb_trades = len(trades)
assert nb_trades == 0
# Custom user buy-timeout is never called
# Custom user entry-timeout is never called
assert freqtrade.strategy.check_entry_timeout.call_count == 0
# Entry adjustment is never called
assert freqtrade.strategy.adjust_entry_price.call_count == 0
@ -5023,7 +5023,7 @@ def test_get_real_amount_in_point(default_conf_usdt, buy_order_fee, fee, mocker,
(8.0, 0.1, 8.0, None),
(8.0, 0.1, 7.9, 0.1),
])
def test_apply_fee_conditional(default_conf_usdt, fee, mocker,
def test_apply_fee_conditional(default_conf_usdt, fee, mocker, caplog,
amount, fee_abs, wallet, amount_exp):
walletmock = mocker.patch('freqtrade.wallets.Wallets.update')
mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=wallet)
@ -5048,6 +5048,60 @@ def test_apply_fee_conditional(default_conf_usdt, fee, mocker,
# Amount is kept as is
assert freqtrade.apply_fee_conditional(trade, 'LTC', amount, fee_abs, order) == amount_exp
assert walletmock.call_count == 1
if fee_abs != 0 and amount_exp is None:
assert log_has_re(r"Fee amount.*Eating.*dust\.", caplog)
@pytest.mark.parametrize('amount,fee_abs,wallet,amount_exp', [
(8.0, 0.0, 16, None),
(8.0, 0.0, 0, None),
(8.0, 0.1, 8, 0.1),
(8.0, 0.1, 20, None),
(8.0, 0.1, 16.0, None),
(8.0, 0.1, 7.9, 0.1),
(8.0, 0.1, 12, 0.1),
(8.0, 0.1, 15.9, 0.1),
])
def test_apply_fee_conditional_multibuy(default_conf_usdt, fee, mocker, caplog,
amount, fee_abs, wallet, amount_exp):
walletmock = mocker.patch('freqtrade.wallets.Wallets.update')
mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=wallet)
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_order_id="123456"
)
# One closed order
order = Order(
ft_order_side='buy',
order_id='10',
ft_pair=trade.pair,
ft_is_open=False,
filled=amount,
status="closed"
)
trade.orders.append(order)
# Add additional order - this should NOT eat into dust unless the wallet was bigger already.
order1 = Order(
ft_order_side='buy',
order_id='100',
ft_pair=trade.pair,
ft_is_open=True,
)
trade.orders.append(order1)
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
walletmock.reset_mock()
# The new trade amount will be 2x amount - fee / wallet will have to be adapted to this.
assert freqtrade.apply_fee_conditional(trade, 'LTC', amount, fee_abs, order1) == amount_exp
assert walletmock.call_count == 1
if fee_abs != 0 and amount_exp is None:
assert log_has_re(r"Fee amount.*Eating.*dust\.", caplog)
@pytest.mark.parametrize("delta, is_high_delta", [

View File

@ -429,6 +429,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
assert pytest.approx(trade.stop_loss) == 1.99 * (1 - 0.1 / leverage)
assert pytest.approx(trade.initial_stop_loss) == 1.96 * (1 - 0.1 / leverage)
assert trade.initial_stop_loss_pct == -0.1
assert pytest.approx(trade.orders[-1].stake_amount) == trade.stake_amount
# 2nd order - not filling
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=120)
@ -473,13 +474,38 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
assert pytest.approx(trade.orders[1].amount) == 30.150753768 * leverage
assert pytest.approx(trade.orders[-1].amount) == 61.538461232 * leverage
# Full exit
mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False)
freqtrade.strategy.custom_exit = MagicMock(return_value='Exit now')
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=2.02)
freqtrade.process()
trade = Trade.get_trades().first()
assert len(trade.orders) == 5
assert trade.orders[-1].side == trade.exit_side
assert trade.orders[-1].status == 'open'
assert trade.orders[-1].price == 2.02
assert pytest.approx(trade.amount) == 91.689215 * leverage
assert pytest.approx(trade.orders[-1].amount) == 91.689215 * leverage
assert freqtrade.strategy.adjust_entry_price.call_count == 0
# Process again, should not adjust entry price
freqtrade.process()
trade = Trade.get_trades().first()
assert len(trade.orders) == 5
assert trade.orders[-1].status == 'open'
assert trade.orders[-1].price == 2.02
# Adjust entry price cannot be called - this is an exit order
assert freqtrade.strategy.adjust_entry_price.call_count == 0
@pytest.mark.parametrize('leverage', [1, 2])
def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, leverage) -> None:
default_conf_usdt['position_adjustment_enable'] = True
spot = leverage == 1
if not spot:
default_conf_usdt['trading_mode'] = 'futures'
default_conf_usdt['margin_mode'] = 'isolated'
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
freqtrade.trading_mode = TradingMode.FUTURES
assert freqtrade.trading_mode == TradingMode.FUTURES if not spot else TradingMode.SPOT
mocker.patch.multiple(
EXMS,
fetch_ticker=ticker_usdt,
@ -487,8 +513,11 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
amount_to_precision=lambda s, x, y: y,
price_to_precision=lambda s, x, y: y,
get_min_pair_stake_amount=MagicMock(return_value=10),
get_funding_fees=MagicMock(return_value=0),
)
mocker.patch(f"{EXMS}.get_max_leverage", return_value=10)
starting_amount = freqtrade.wallets.get_total('USDT')
assert starting_amount == 1000
patch_get_signal(freqtrade)
freqtrade.strategy.leverage = MagicMock(return_value=leverage)
@ -498,8 +527,14 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
trade = Trade.get_trades().first()
assert len(trade.orders) == 1
assert pytest.approx(trade.stake_amount) == 60
assert trade.leverage == leverage
assert pytest.approx(trade.amount) == 30.0 * leverage
assert trade.open_rate == 2.0
assert pytest.approx(freqtrade.wallets.get_free('USDT')) == starting_amount - 60
if spot:
assert pytest.approx(freqtrade.wallets.get_total('USDT')) == starting_amount - 60
else:
assert freqtrade.wallets.get_total('USDT') == starting_amount
# Too small size
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-59)
@ -521,6 +556,15 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
assert pytest.approx(trade.amount) == 20.099 * leverage
assert trade.open_rate == 2.0
assert trade.is_open
assert trade.realized_profit > 0.098 * leverage
expected_profit = starting_amount - 40.1980 + trade.realized_profit
assert pytest.approx(freqtrade.wallets.get_free('USDT')) == expected_profit
if spot:
assert pytest.approx(freqtrade.wallets.get_total('USDT')) == expected_profit
else:
# total won't change in futures mode, only free / used will.
assert freqtrade.wallets.get_total('USDT') == starting_amount + trade.realized_profit
caplog.clear()
# Sell more than what we got (we got ~20 coins left)
@ -545,3 +589,10 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
assert pytest.approx(trade.stake_amount) == 40.198
assert trade.is_open
assert log_has_re('Amount to exit is 0.0 due to exchange limits - not exiting.', caplog)
expected_profit = starting_amount - 40.1980 + trade.realized_profit
assert pytest.approx(freqtrade.wallets.get_free('USDT')) == expected_profit
if spot:
assert pytest.approx(freqtrade.wallets.get_total('USDT')) == expected_profit
else:
# total won't change in futures mode, only free / used will.
assert freqtrade.wallets.get_total('USDT') == starting_amount + trade.realized_profit

View File

@ -8,7 +8,7 @@ import pandas as pd
import pytest
from freqtrade.misc import (dataframe_to_json, decimals_per_coin, deep_merge_dicts, file_dump_json,
file_load_json, json_to_dataframe, pair_to_filename,
file_load_json, is_file_in_dir, json_to_dataframe, pair_to_filename,
parse_db_uri_for_logging, plural, render_template,
render_template_with_fallback, round_coin_value, safe_value_fallback,
safe_value_fallback2)
@ -64,6 +64,24 @@ def test_file_load_json(mocker, testdatadir) -> None:
assert ret
def test_is_file_in_dir(tmp_path):
# Create a temporary directory and file
dir_path = tmp_path / "subdir"
dir_path.mkdir()
file_path = dir_path / "test.txt"
file_path.touch()
# Test that the function returns True when the file is in the directory
assert is_file_in_dir(file_path, dir_path) is True
# Test that the function returns False when the file is not in the directory
assert is_file_in_dir(file_path, tmp_path) is False
file_path2 = tmp_path / "../../test2.txt"
assert is_file_in_dir(file_path2, tmp_path) is False
@pytest.mark.parametrize("pair,expected_result", [
("ETH/BTC", 'ETH_BTC'),
("ETH/USDT", 'ETH_USDT'),

View File

@ -8,7 +8,8 @@ from sqlalchemy import select
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
from freqtrade.exceptions import DependencyException
from freqtrade.persistence import Trade
from tests.conftest import EXMS, create_mock_trades, get_patched_freqtradebot, patch_wallet
from tests.conftest import (EXMS, create_mock_trades, create_mock_trades_usdt,
get_patched_freqtradebot, patch_wallet)
def test_sync_wallet_at_boot(mocker, default_conf):
@ -341,6 +342,33 @@ def test_sync_wallet_futures_live(mocker, default_conf):
assert 'ETH/USDT:USDT' not in freqtrade.wallets._positions
def test_sync_wallet_dry(mocker, default_conf_usdt, fee):
default_conf_usdt['dry_run'] = True
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
assert len(freqtrade.wallets._wallets) == 1
assert len(freqtrade.wallets._positions) == 0
assert freqtrade.wallets.get_total('USDT') == 1000
create_mock_trades_usdt(fee, is_short=None)
freqtrade.wallets.update()
assert len(freqtrade.wallets._wallets) == 5
assert len(freqtrade.wallets._positions) == 0
bal = freqtrade.wallets.get_all_balances()
assert bal['NEO'].total == 10
assert bal['XRP'].total == 10
assert bal['LTC'].total == 2
assert bal['USDT'].total == 922.74
assert freqtrade.wallets.get_starting_balance() == default_conf_usdt['dry_run_wallet']
total = freqtrade.wallets.get_total('LTC')
free = freqtrade.wallets.get_free('LTC')
used = freqtrade.wallets.get_used('LTC')
assert free != 0
assert free + used == total
def test_sync_wallet_futures_dry(mocker, default_conf, fee):
default_conf['dry_run'] = True
default_conf['trading_mode'] = 'futures'