mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 02:12:01 +00:00
commit
d104fb13a0
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
@ -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__
|
||||
|
|
|
@ -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.17
|
||||
- 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.
BIN
build_helpers/TA_Lib-0.4.27-cp310-cp310-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.27-cp310-cp310-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.27-cp311-cp311-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.27-cp311-cp311-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.27-cp38-cp38-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.27-cp38-cp38-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.27-cp39-cp39-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.27-cp39-cp39-win_amd64.whl
Normal file
Binary file not shown.
|
@ -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 .
|
||||
|
|
Binary file not shown.
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -682,16 +682,14 @@ To use a proxy for exchange connections - you will have to define the proxies as
|
|||
{
|
||||
"exchange": {
|
||||
"ccxt_config": {
|
||||
"aiohttp_proxy": "http://addr:port",
|
||||
"proxies": {
|
||||
"http": "http://addr:port",
|
||||
"https": "http://addr:port"
|
||||
},
|
||||
"httpsProxy": "http://addr:port",
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For more information on available proxy types, please consult the [ccxt proxy documentation](https://docs.ccxt.com/#/README?id=proxy).
|
||||
|
||||
## Next step
|
||||
|
||||
Now you have configured your config.json, the next step is to [start your bot](bot-usage.md).
|
||||
|
|
|
@ -453,7 +453,13 @@ Once the PR against stable is merged (best right after merging):
|
|||
* Use the button "Draft a new release" in the Github UI (subsection releases).
|
||||
* Use the version-number specified as tag.
|
||||
* Use "stable" as reference (this step comes after the above PR is merged).
|
||||
* Use the above changelog as release comment (as codeblock)
|
||||
* Use the above changelog as release comment (as codeblock).
|
||||
* Use the below snippet for the new release
|
||||
|
||||
??? Tip "Release template"
|
||||
````
|
||||
--8<-- "includes/release_template.md"
|
||||
````
|
||||
|
||||
## Releases
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -184,6 +184,8 @@ The RemotePairList is defined in the pairlists section of the configuration sett
|
|||
"pairlists": [
|
||||
{
|
||||
"method": "RemotePairList",
|
||||
"mode": "whitelist",
|
||||
"processing_mode": "filter",
|
||||
"pairlist_url": "https://example.com/pairlist",
|
||||
"number_assets": 10,
|
||||
"refresh_period": 1800,
|
||||
|
@ -194,6 +196,14 @@ The RemotePairList is defined in the pairlists section of the configuration sett
|
|||
]
|
||||
```
|
||||
|
||||
The optional `mode` option specifies if the pairlist should be used as a `blacklist` or as a `whitelist`. The default value is "whitelist".
|
||||
|
||||
The optional `processing_mode` option in the RemotePairList configuration determines how the retrieved pairlist is processed. It can have two values: "filter" or "append".
|
||||
|
||||
In "filter" mode, the retrieved pairlist is used as a filter. Only the pairs present in both the original pairlist and the retrieved pairlist are included in the final pairlist. Other pairs are filtered out.
|
||||
|
||||
In "append" mode, the retrieved pairlist is added to the original pairlist. All pairs from both lists are included in the final pairlist without any filtering.
|
||||
|
||||
The `pairlist_url` option specifies the URL of the remote server where the pairlist is located, or the path to a local file (if file:/// is prepended). This allows the user to use either a remote server or a local file as the source for the pairlist.
|
||||
|
||||
The user is responsible for providing a server or local file that returns a JSON object with the following structure:
|
||||
|
@ -201,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
|
||||
}
|
||||
```
|
||||
|
||||
|
|
37
docs/includes/release_template.md
Normal file
37
docs/includes/release_template.md
Normal file
|
@ -0,0 +1,37 @@
|
|||
## Highlighted changes
|
||||
|
||||
- ...
|
||||
|
||||
### How to update
|
||||
|
||||
As always, you can update your bot using one of the following commands:
|
||||
|
||||
#### docker-compose
|
||||
|
||||
```bash
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
#### Installation via setup script
|
||||
|
||||
```
|
||||
# Deactivate venv and run
|
||||
./setup.sh --update
|
||||
```
|
||||
|
||||
#### Plain native installation
|
||||
|
||||
```
|
||||
git pull
|
||||
pip install -U -r requirements.txt
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Expand full changelog</summary>
|
||||
|
||||
```
|
||||
<Paste your changelog here>
|
||||
```
|
||||
|
||||
</details>
|
11
docs/includes/showcase.md
Normal file
11
docs/includes/showcase.md
Normal 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.
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
markdown==3.3.7
|
||||
mkdocs==1.4.3
|
||||
mkdocs-material==9.1.17
|
||||
mkdocs-material==9.1.19
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==10.0.1
|
||||
pymdown-extensions==10.1
|
||||
jinja2==3.1.2
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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**
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
""" Freqtrade bot """
|
||||
__version__ = '2023.6'
|
||||
__version__ = '2023.7'
|
||||
|
||||
if 'dev' in __version__:
|
||||
from pathlib import Path
|
||||
|
|
|
@ -67,8 +67,7 @@ ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
|||
|
||||
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase", "exchange"]
|
||||
|
||||
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "trading_mode",
|
||||
"candle_types"]
|
||||
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "trading_mode", "candle_types"]
|
||||
|
||||
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"]
|
||||
|
||||
|
|
|
@ -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']
|
||||
},
|
||||
{
|
||||
|
|
|
@ -381,7 +381,7 @@ AVAILABLE_CLI_OPTIONS = {
|
|||
),
|
||||
"candle_types": Arg(
|
||||
'--candle-types',
|
||||
help='Select candle type to use',
|
||||
help='Select candle type to convert. Defaults to all available types.',
|
||||
choices=[c.value for c in CandleType],
|
||||
nargs='+',
|
||||
),
|
||||
|
@ -450,14 +450,12 @@ AVAILABLE_CLI_OPTIONS = {
|
|||
),
|
||||
"exchange": Arg(
|
||||
'--exchange',
|
||||
help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). '
|
||||
f'Only valid if no config is provided.',
|
||||
help='Exchange name. Only valid if no config is provided.',
|
||||
),
|
||||
"timeframes": Arg(
|
||||
'-t', '--timeframes',
|
||||
help='Specify which tickers to download. Space-separated list. '
|
||||
'Default: `1m 5m`.',
|
||||
default=['1m', '5m'],
|
||||
nargs='+',
|
||||
),
|
||||
"prepend_data": Arg(
|
||||
|
|
|
@ -4,10 +4,10 @@ from collections import defaultdict
|
|||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.configuration import TimeRange, setup_utils_configuration
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, Config
|
||||
from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
|
||||
from freqtrade.data.history import convert_trades_to_ohlcv, download_data_main
|
||||
from freqtrade.enums import CandleType, RunMode, TradingMode
|
||||
from freqtrade.enums import RunMode, TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
|
@ -57,6 +57,8 @@ def start_convert_trades(args: Dict[str, Any]) -> None:
|
|||
raise OperationalException(
|
||||
"Downloading data requires a list of pairs. "
|
||||
"Please check the documentation on how to configure this.")
|
||||
if 'timeframes' not in config:
|
||||
config['timeframes'] = DL_DATA_TIMEFRAMES
|
||||
|
||||
# Init exchange
|
||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||
|
@ -86,11 +88,10 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
|||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
if ohlcv:
|
||||
migrate_binance_futures_data(config)
|
||||
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', ['spot'])]
|
||||
for candle_type in candle_types:
|
||||
convert_ohlcv_format(config,
|
||||
convert_from=args['format_from'], convert_to=args['format_to'],
|
||||
erase=args['erase'], candle_type=candle_type)
|
||||
convert_ohlcv_format(config,
|
||||
convert_from=args['format_from'],
|
||||
convert_to=args['format_to'],
|
||||
erase=args['erase'])
|
||||
else:
|
||||
convert_trades_format(config,
|
||||
convert_from=args['format_from'], convert_to=args['format_to'],
|
||||
|
|
8
freqtrade/configuration/detect_environment.py
Normal file
8
freqtrade/configuration/detect_environment.py
Normal 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'
|
|
@ -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(
|
||||
|
|
|
@ -10,7 +10,6 @@ from freqtrade.enums import CandleType, PriceType, RPCMessageType
|
|||
|
||||
DOCS_LINK = "https://www.freqtrade.io/en/stable"
|
||||
DEFAULT_CONFIG = 'config.json'
|
||||
DEFAULT_EXCHANGE = 'bittrex'
|
||||
PROCESS_THROTTLE_SECS = 5 # sec
|
||||
HYPEROPT_EPOCH = 100 # epochs
|
||||
RETRY_TIMEOUT = 30 # sec
|
||||
|
@ -66,6 +65,7 @@ TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
|||
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
|
||||
FULL_DATAFRAME_THRESHOLD = 100
|
||||
CUSTOM_TAG_MAX_LENGTH = 255
|
||||
DL_DATA_TIMEFRAMES = ['1m', '5m']
|
||||
|
||||
ENV_VAR_PREFIX = 'FREQTRADE__'
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -11,7 +11,7 @@ import pandas as pd
|
|||
from pandas import DataFrame, to_datetime
|
||||
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, Config, TradeList
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.enums import CandleType, TradingMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -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
|
||||
|
@ -264,7 +270,6 @@ def convert_ohlcv_format(
|
|||
convert_from: str,
|
||||
convert_to: str,
|
||||
erase: bool,
|
||||
candle_type: CandleType
|
||||
):
|
||||
"""
|
||||
Convert OHLCV from one format to another
|
||||
|
@ -272,7 +277,6 @@ def convert_ohlcv_format(
|
|||
:param convert_from: Source format
|
||||
:param convert_to: Target format
|
||||
:param erase: Erase source data (does not apply if source and target format are identical)
|
||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||
"""
|
||||
from freqtrade.data.history.idatahandler import get_datahandler
|
||||
src = get_datahandler(config['datadir'], convert_from)
|
||||
|
@ -280,37 +284,45 @@ def convert_ohlcv_format(
|
|||
timeframes = config.get('timeframes', [config.get('timeframe')])
|
||||
logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}")
|
||||
|
||||
if 'pairs' not in config:
|
||||
config['pairs'] = []
|
||||
# Check timeframes or fall back to timeframe.
|
||||
for timeframe in timeframes:
|
||||
config['pairs'].extend(src.ohlcv_get_pairs(
|
||||
config['datadir'],
|
||||
timeframe,
|
||||
candle_type=candle_type
|
||||
))
|
||||
config['pairs'] = sorted(set(config['pairs']))
|
||||
logger.info(f"Converting candle (OHLCV) data for {config['pairs']}")
|
||||
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', [
|
||||
c.value for c in CandleType])]
|
||||
logger.info(candle_types)
|
||||
paircombs = src.ohlcv_get_available_data(config['datadir'], TradingMode.SPOT)
|
||||
paircombs.extend(src.ohlcv_get_available_data(config['datadir'], TradingMode.FUTURES))
|
||||
|
||||
for timeframe in timeframes:
|
||||
for pair in config['pairs']:
|
||||
data = src.ohlcv_load(pair=pair, timeframe=timeframe,
|
||||
timerange=None,
|
||||
fill_missing=False,
|
||||
drop_incomplete=False,
|
||||
startup_candles=0,
|
||||
candle_type=candle_type)
|
||||
logger.info(f"Converting {len(data)} {timeframe} {candle_type} candles for {pair}")
|
||||
if len(data) > 0:
|
||||
trg.ohlcv_store(
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
data=data,
|
||||
candle_type=candle_type
|
||||
)
|
||||
if erase and convert_from != convert_to:
|
||||
logger.info(f"Deleting source data for {pair} / {timeframe}")
|
||||
src.ohlcv_purge(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||
if 'pairs' in config:
|
||||
# Filter pairs
|
||||
paircombs = [comb for comb in paircombs if comb[0] in config['pairs']]
|
||||
|
||||
if 'timeframes' in config:
|
||||
paircombs = [comb for comb in paircombs if comb[1] in config['timeframes']]
|
||||
paircombs = [comb for comb in paircombs if comb[2] in candle_types]
|
||||
|
||||
paircombs = sorted(paircombs, key=lambda x: (x[0], x[1], x[2].value))
|
||||
|
||||
formatted_paircombs = '\n'.join([f"{pair}, {timeframe}, {candle_type}"
|
||||
for pair, timeframe, candle_type in paircombs])
|
||||
|
||||
logger.info(f"Converting candle (OHLCV) data for the following pair combinations:\n"
|
||||
f"{formatted_paircombs}")
|
||||
for pair, timeframe, candle_type in paircombs:
|
||||
data = src.ohlcv_load(pair=pair, timeframe=timeframe,
|
||||
timerange=None,
|
||||
fill_missing=False,
|
||||
drop_incomplete=False,
|
||||
startup_candles=0,
|
||||
candle_type=candle_type)
|
||||
logger.info(f"Converting {len(data)} {timeframe} {candle_type} candles for {pair}")
|
||||
if len(data) > 0:
|
||||
trg.ohlcv_store(
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
data=data,
|
||||
candle_type=candle_type
|
||||
)
|
||||
if erase and convert_from != convert_to:
|
||||
logger.info(f"Deleting source data for {pair} / {timeframe}")
|
||||
src.ohlcv_purge(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||
|
||||
|
||||
def reduce_dataframe_footprint(df: DataFrame) -> DataFrame:
|
||||
|
|
|
@ -7,15 +7,16 @@ from typing import Dict, List, Optional, Tuple
|
|||
from pandas import DataFrame, concat
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMNS, Config
|
||||
from freqtrade.constants import (DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMNS,
|
||||
DL_DATA_TIMEFRAMES, Config)
|
||||
from freqtrade.data.converter import (clean_ohlcv_dataframe, ohlcv_to_dataframe,
|
||||
trades_remove_duplicates, trades_to_ohlcv)
|
||||
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.misc import format_ms_time
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
||||
from freqtrade.util import format_ms_time
|
||||
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
||||
|
||||
|
||||
|
@ -354,7 +355,7 @@ def _download_trades_history(exchange: Exchange,
|
|||
trades = []
|
||||
|
||||
if not since:
|
||||
since = int((datetime.now() - timedelta(days=-new_pairs_days)).timestamp()) * 1000
|
||||
since = int((datetime.now() - timedelta(days=new_pairs_days)).timestamp()) * 1000
|
||||
|
||||
from_id = trades[-1][1] if trades else None
|
||||
if trades and since < trades[-1][0]:
|
||||
|
@ -512,6 +513,8 @@ def download_data_main(config: Config) -> None:
|
|||
]
|
||||
|
||||
expanded_pairs = dynamic_expand_pairlist(config, available_pairs)
|
||||
if 'timeframes' not in config:
|
||||
config['timeframes'] = DL_DATA_TIMEFRAMES
|
||||
|
||||
# Manual validations of relevant settings
|
||||
if not config['exchange'].get('skip_pair_validation', False):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
@ -28,7 +28,7 @@ class Bybit(Exchange):
|
|||
|
||||
_ft_has: Dict = {
|
||||
"ohlcv_candle_limit": 200,
|
||||
"ohlcv_has_history": False,
|
||||
"ohlcv_has_history": True,
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
"ohlcv_has_history": True,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -231,7 +231,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
self.manage_open_orders()
|
||||
|
||||
# Protect from collisions with force_exit.
|
||||
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
|
||||
# Without this, freqtrade may try to recreate stoploss_on_exchange orders
|
||||
# while exiting is in process, since telegram messages arrive in an different thread.
|
||||
with self._exit_lock:
|
||||
trades = Trade.get_open_trades()
|
||||
|
@ -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)
|
||||
|
|
|
@ -3,7 +3,6 @@ Various tool function for Freqtrade and scripts
|
|||
"""
|
||||
import gzip
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, List, Mapping, Optional, TextIO, Union
|
||||
from urllib.parse import urlparse
|
||||
|
@ -117,20 +116,19 @@ 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, '_')
|
||||
return pair
|
||||
|
||||
|
||||
def format_ms_time(date: int) -> str:
|
||||
"""
|
||||
convert MS date to readable format.
|
||||
: epoch-string in ms
|
||||
"""
|
||||
return datetime.fromtimestamp(date / 1000.0).strftime('%Y-%m-%dT%H:%M:%S')
|
||||
|
||||
|
||||
def deep_merge_dicts(source, destination, allow_null_overrides: bool = True):
|
||||
"""
|
||||
Values from Source override destination, destination is returned (and modified!!)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')} / "
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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[List["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,11 +121,20 @@ 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}, order_id={self.order_id}, trade_id={self.ft_trade_id}, "
|
||||
return (f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, "
|
||||
f"side={self.side}, filled={self.safe_filled}, price={self.safe_price}, "
|
||||
f"order_type={self.order_type}, status={self.status})")
|
||||
f"status={self.status}, date={self.order_date:{DATETIME_PRINT_FORMAT}})")
|
||||
|
||||
def update_from_ccxt_object(self, order):
|
||||
"""
|
||||
|
@ -212,6 +223,7 @@ class Order(ModelBase):
|
|||
'order_type': self.order_type,
|
||||
'price': self.price,
|
||||
'remaining': self.remaining,
|
||||
'ft_fee_base': self.ft_fee_base,
|
||||
})
|
||||
return resp
|
||||
|
||||
|
@ -1298,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):
|
||||
|
@ -1654,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"],
|
||||
|
@ -1708,6 +1724,7 @@ class Trade(ModelBase, LocalTrade):
|
|||
|
||||
order_obj = Order(
|
||||
amount=order["amount"],
|
||||
ft_amount=order["amount"],
|
||||
ft_order_side=order["ft_order_side"],
|
||||
ft_pair=order["pair"],
|
||||
ft_is_open=order["is_open"],
|
||||
|
@ -1722,6 +1739,7 @@ class Trade(ModelBase, LocalTrade):
|
|||
if order["order_filled_timestamp"] else None),
|
||||
order_type=order["order_type"],
|
||||
price=order["price"],
|
||||
ft_price=order["price"],
|
||||
remaining=order["remaining"],
|
||||
)
|
||||
trade.orders.append(order_obj)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -89,10 +89,10 @@ class PerformanceFilter(IPairList):
|
|||
# Sort the list using:
|
||||
# - primarily performance (high to low)
|
||||
# - then count (low to high, so as to favor same performance with fewer trades)
|
||||
# - then pair name alphametically
|
||||
# - then by prior index, keeping original sorting order
|
||||
sorted_df = list_df.merge(performance, on='pair', how='left')\
|
||||
.fillna(0).sort_values(by=['count', 'prior_idx'], ascending=True)\
|
||||
.sort_values(by=['profit_ratio'], ascending=False)
|
||||
.fillna(0).sort_values(by=['profit_ratio', 'count', 'prior_idx'],
|
||||
ascending=[False, True, True])
|
||||
if self._min_profit is not None:
|
||||
removed = sorted_df[sorted_df['profit_ratio'] < self._min_profit]
|
||||
for _, row in removed.iterrows():
|
||||
|
|
|
@ -3,19 +3,21 @@ 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
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -40,6 +42,8 @@ class RemotePairList(IPairList):
|
|||
'`pairlist_url` not specified. Please check your configuration '
|
||||
'for "pairlist.config.pairlist_url"')
|
||||
|
||||
self._mode = self._pairlistconfig.get('mode', 'whitelist')
|
||||
self._processing_mode = self._pairlistconfig.get('processing_mode', 'filter')
|
||||
self._number_pairs = self._pairlistconfig['number_assets']
|
||||
self._refresh_period: int = self._pairlistconfig.get('refresh_period', 1800)
|
||||
self._keep_pairlist_on_failure = self._pairlistconfig.get('keep_pairlist_on_failure', True)
|
||||
|
@ -50,6 +54,21 @@ class RemotePairList(IPairList):
|
|||
self._init_done = False
|
||||
self._last_pairlist: List[Any] = list()
|
||||
|
||||
if self._mode not in ['whitelist', 'blacklist']:
|
||||
raise OperationalException(
|
||||
'`mode` not configured correctly. Supported Modes '
|
||||
'are "whitelist","blacklist"')
|
||||
|
||||
if self._processing_mode not in ['filter', 'append']:
|
||||
raise OperationalException(
|
||||
'`processing_mode` not configured correctly. Supported Modes '
|
||||
'are "filter","append"')
|
||||
|
||||
if self._pairlist_pos == 0 and self._mode == 'blacklist':
|
||||
raise OperationalException(
|
||||
'A `blacklist` mode RemotePairList can not be on the first '
|
||||
'position of your pairlist.')
|
||||
|
||||
@property
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
|
@ -67,23 +86,37 @@ class RemotePairList(IPairList):
|
|||
|
||||
@staticmethod
|
||||
def description() -> str:
|
||||
return "Retrieve pairs from a remote API."
|
||||
return "Retrieve pairs from a remote API or local file."
|
||||
|
||||
@staticmethod
|
||||
def available_parameters() -> Dict[str, PairlistParameter]:
|
||||
return {
|
||||
"number_assets": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Number of assets",
|
||||
"help": "Number of assets to use from the pairlist.",
|
||||
},
|
||||
"pairlist_url": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "URL to fetch pairlist from",
|
||||
"help": "URL to fetch pairlist from",
|
||||
},
|
||||
"number_assets": {
|
||||
"type": "number",
|
||||
"default": 30,
|
||||
"description": "Number of assets",
|
||||
"help": "Number of assets to use from the pairlist.",
|
||||
},
|
||||
"mode": {
|
||||
"type": "option",
|
||||
"default": "whitelist",
|
||||
"options": ["whitelist", "blacklist"],
|
||||
"description": "Pairlist mode",
|
||||
"help": "Should this pairlist operate as a whitelist or blacklist?",
|
||||
},
|
||||
"processing_mode": {
|
||||
"type": "option",
|
||||
"default": "filter",
|
||||
"options": ["filter", "append"],
|
||||
"description": "Processing mode",
|
||||
"help": "Append pairs to incomming pairlist or filter them?",
|
||||
},
|
||||
**IPairList.refresh_period_parameter(),
|
||||
"keep_pairlist_on_failure": {
|
||||
"type": "boolean",
|
||||
|
@ -204,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)
|
||||
|
@ -223,6 +256,7 @@ class RemotePairList(IPairList):
|
|||
|
||||
self.log_once(f"Fetched pairs: {pairlist}", logger.debug)
|
||||
|
||||
pairlist = expand_pairlist(pairlist, list(self._exchange.get_markets().keys()))
|
||||
pairlist = self._whitelist_for_active_markets(pairlist)
|
||||
pairlist = pairlist[:self._number_pairs]
|
||||
|
||||
|
@ -250,6 +284,23 @@ class RemotePairList(IPairList):
|
|||
:return: new whitelist
|
||||
"""
|
||||
rpl_pairlist = self.gen_pairlist(tickers)
|
||||
merged_list = pairlist + rpl_pairlist
|
||||
merged_list = sorted(set(merged_list), key=merged_list.index)
|
||||
merged_list = []
|
||||
filtered = []
|
||||
|
||||
if self._mode == "whitelist":
|
||||
if self._processing_mode == "filter":
|
||||
merged_list = [pair for pair in pairlist if pair in rpl_pairlist]
|
||||
elif self._processing_mode == "append":
|
||||
merged_list = pairlist + rpl_pairlist
|
||||
merged_list = sorted(set(merged_list), key=merged_list.index)
|
||||
else:
|
||||
for pair in pairlist:
|
||||
if pair not in rpl_pairlist:
|
||||
merged_list.append(pair)
|
||||
else:
|
||||
filtered.append(pair)
|
||||
if filtered:
|
||||
self.log_once(f"Blacklist - Filtered out pairs: {filtered}", logger.info)
|
||||
|
||||
merged_list = merged_list[:self._number_pairs]
|
||||
return merged_list
|
||||
|
|
|
@ -13,9 +13,8 @@ from freqtrade.constants import Config, ListPairsWithTimeframes
|
|||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.misc import format_ms_time
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.util import dt_now
|
||||
from freqtrade.util import dt_now, format_ms_time
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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]
|
||||
|
@ -238,6 +241,7 @@ class OrderSchema(BaseModel):
|
|||
is_open: bool
|
||||
order_timestamp: Optional[int]
|
||||
order_filled_timestamp: Optional[int]
|
||||
ft_fee_base: Optional[float]
|
||||
|
||||
|
||||
class TradeSchema(BaseModel):
|
||||
|
|
|
@ -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__,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -25,6 +25,7 @@ coingecko_mapping = {
|
|||
'bnb': 'binancecoin',
|
||||
'sol': 'solana',
|
||||
'usdt': 'tether',
|
||||
'busd': 'binance-usd',
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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.get('dataformat_ohlcv', 'json'),
|
||||
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
|
||||
|
|
|
@ -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 += (
|
||||
|
|
|
@ -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}')
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from freqtrade.util.datetime_helpers import (dt_floor_day, dt_from_ts, dt_humanize, dt_now, dt_ts,
|
||||
dt_utc, shorten_date)
|
||||
dt_utc, format_ms_time, shorten_date)
|
||||
from freqtrade.util.ft_precise import FtPrecise
|
||||
from freqtrade.util.periodic_cache import PeriodicCache
|
||||
|
||||
|
@ -7,11 +7,12 @@ from freqtrade.util.periodic_cache import PeriodicCache
|
|||
__all__ = [
|
||||
'dt_floor_day',
|
||||
'dt_from_ts',
|
||||
'dt_humanize',
|
||||
'dt_now',
|
||||
'dt_ts',
|
||||
'dt_utc',
|
||||
'dt_humanize',
|
||||
'shorten_date',
|
||||
'format_ms_time',
|
||||
'FtPrecise',
|
||||
'PeriodicCache',
|
||||
'shorten_date',
|
||||
]
|
||||
|
|
|
@ -61,3 +61,11 @@ def dt_humanize(dt: datetime, **kwargs) -> str:
|
|||
:param kwargs: kwargs to pass to arrow's humanize()
|
||||
"""
|
||||
return arrow.get(dt).humanize(**kwargs)
|
||||
|
||||
|
||||
def format_ms_time(date: int) -> str:
|
||||
"""
|
||||
convert MS date to readable format.
|
||||
: epoch-string in ms
|
||||
"""
|
||||
return datetime.fromtimestamp(date / 1000.0).strftime('%Y-%m-%dT%H:%M:%S')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -7,24 +7,24 @@
|
|||
-r docs/requirements-docs.txt
|
||||
|
||||
coveralls==3.3.1
|
||||
ruff==0.0.275
|
||||
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
|
||||
isort==5.12.0
|
||||
# For datetime mocking
|
||||
time-machine==2.10.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
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
torch==2.0.1
|
||||
#until these branches will be released we can use this
|
||||
gymnasium==0.28.1
|
||||
stable_baselines3==2.0.0a13
|
||||
stable_baselines3==2.0.0
|
||||
sb3_contrib>=2.0.0a9
|
||||
# Progress bar for stable-baselines3 and sb3-contrib
|
||||
tqdm==4.65.0
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
|
||||
# Required for freqai
|
||||
scikit-learn==1.1.3
|
||||
joblib==1.2.0
|
||||
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.6
|
||||
datasieve==0.1.7
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
-r requirements.txt
|
||||
|
||||
# Required for hyperopt
|
||||
scipy==1.10.1
|
||||
scipy==1.11.1; python_version >= '3.9'
|
||||
scipy==1.10.1; python_version < '3.9'
|
||||
scikit-learn==1.1.3
|
||||
scikit-optimize==0.9.0
|
||||
filelock==3.12.2
|
||||
|
|
|
@ -1,30 +1,31 @@
|
|||
numpy==1.24.3
|
||||
pandas==2.0.2
|
||||
numpy==1.25.1; python_version > '3.8'
|
||||
numpy==1.24.3; python_version <= '3.8'
|
||||
pandas==2.0.3
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==3.1.44
|
||||
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.17
|
||||
python-telegram-bot==20.3
|
||||
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.17.3
|
||||
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
|
||||
jinja2==3.1.2
|
||||
tables==3.8.0
|
||||
blosc==1.11.1
|
||||
joblib==1.2.0
|
||||
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
|
||||
|
@ -32,16 +33,16 @@ py_find_1st==1.1.5
|
|||
# Load ticker files 30% faster
|
||||
python-rapidjson==1.10
|
||||
# Properly format api responses
|
||||
orjson==3.9.1
|
||||
orjson==3.9.2
|
||||
|
||||
# Notify systemd
|
||||
sdnotify==0.3.2
|
||||
|
||||
# API Server
|
||||
fastapi==0.99.1
|
||||
pydantic==1.10.9
|
||||
uvicorn==0.22.0
|
||||
pyjwt==2.7.0
|
||||
fastapi==0.100.0
|
||||
pydantic==1.10.11
|
||||
uvicorn==0.23.1
|
||||
pyjwt==2.8.0
|
||||
aiofiles==23.1.0
|
||||
psutil==5.9.5
|
||||
|
||||
|
@ -49,7 +50,7 @@ psutil==5.9.5
|
|||
colorama==0.4.6
|
||||
# Building config files interactively
|
||||
questionary==1.10.0
|
||||
prompt-toolkit==3.0.38
|
||||
prompt-toolkit==3.0.39
|
||||
# Extensions to datetime library
|
||||
python-dateutil==2.8.2
|
||||
|
||||
|
|
4
setup.py
4
setup.py
|
@ -70,7 +70,7 @@ setup(
|
|||
],
|
||||
install_requires=[
|
||||
# from requirements.txt
|
||||
'ccxt>=3.0.0',
|
||||
'ccxt>=4.0.0',
|
||||
'SQLAlchemy>=2.0.6',
|
||||
'python-telegram-bot>=20.1',
|
||||
'arrow>=1.0.0',
|
||||
|
@ -97,7 +97,7 @@ setup(
|
|||
'rich',
|
||||
'pyarrow; platform_machine != "armv7l"',
|
||||
'fastapi',
|
||||
'pydantic>=1.8.0',
|
||||
'pydantic>=1.8.0,<2.0',
|
||||
'uvicorn',
|
||||
'psutil',
|
||||
'pyjwt',
|
||||
|
|
|
@ -3001,85 +3001,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"
|
||||
|
@ -3091,13 +3091,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"
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 = [
|
||||
|
@ -315,6 +382,8 @@ def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir, file_base, cand
|
|||
files_new.append(file_new)
|
||||
|
||||
default_conf['datadir'] = tmpdir1
|
||||
default_conf['candle_types'] = [candletype]
|
||||
|
||||
if candletype == CandleType.SPOT:
|
||||
default_conf['pairs'] = ['XRP/ETH', 'XRP/USDT', 'UNITTEST/USDT']
|
||||
else:
|
||||
|
@ -328,7 +397,6 @@ def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir, file_base, cand
|
|||
convert_from='json',
|
||||
convert_to='jsongz',
|
||||
erase=False,
|
||||
candle_type=candletype
|
||||
)
|
||||
for file in (files_temp + files_new):
|
||||
assert file.exists()
|
||||
|
@ -342,7 +410,6 @@ def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir, file_base, cand
|
|||
convert_from='jsongz',
|
||||
convert_to='json',
|
||||
erase=True,
|
||||
candle_type=candletype
|
||||
)
|
||||
for file in (files_temp):
|
||||
assert file.exists()
|
||||
|
|
|
@ -43,6 +43,7 @@ EXCHANGES = {
|
|||
'hasQuoteVolumeFutures': True,
|
||||
'leverage_tiers_public': False,
|
||||
'leverage_in_spot_market': False,
|
||||
'trades_lookback_hours': 4,
|
||||
'private_methods': [
|
||||
'fapiPrivateGetPositionSideDual',
|
||||
'fapiPrivateGetMultiAssetsMargin'
|
||||
|
@ -92,12 +93,13 @@ EXCHANGES = {
|
|||
}]
|
||||
},
|
||||
'kraken': {
|
||||
'pair': 'BTC/USDT',
|
||||
'stake_currency': 'USDT',
|
||||
'pair': 'BTC/USD',
|
||||
'stake_currency': 'USD',
|
||||
'hasQuoteVolume': True,
|
||||
'timeframe': '1h',
|
||||
'leverage_tiers_public': False,
|
||||
'leverage_in_spot_market': True,
|
||||
'trades_lookback_hours': 12,
|
||||
},
|
||||
'kucoin': {
|
||||
'pair': 'XRP/USDT',
|
||||
|
@ -291,11 +293,7 @@ def set_test_proxy(config: Config, use_proxy: bool) -> Config:
|
|||
if use_proxy and (proxy := os.environ.get('CI_WEB_PROXY')):
|
||||
config1 = deepcopy(config)
|
||||
config1['exchange']['ccxt_config'] = {
|
||||
"aiohttp_proxy": proxy,
|
||||
'proxies': {
|
||||
'https': proxy,
|
||||
'http': proxy,
|
||||
}
|
||||
"httpsProxy": proxy,
|
||||
}
|
||||
return config1
|
||||
|
||||
|
@ -546,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")
|
||||
|
@ -640,7 +640,21 @@ class TestCCXTExchange:
|
|||
assert isinstance(funding_fee, float)
|
||||
# assert funding_fee > 0
|
||||
|
||||
# TODO: tests fetch_trades (?)
|
||||
def test_ccxt__async_get_trade_history(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchangename = exchange
|
||||
if not (lookback := EXCHANGES[exchangename].get('trades_lookback_hours')):
|
||||
pytest.skip('test_fetch_trades not enabled for this exchange')
|
||||
pair = EXCHANGES[exchangename]['pair']
|
||||
since = int((datetime.now(timezone.utc) - timedelta(hours=lookback)).timestamp() * 1000)
|
||||
res = exch.loop.run_until_complete(
|
||||
exch._async_get_trade_history(pair, since, None, None)
|
||||
)
|
||||
assert len(res) == 2
|
||||
res_pair, res_trades = res
|
||||
assert res_pair == pair
|
||||
assert isinstance(res_trades, list)
|
||||
assert res_trades[0][0] >= since
|
||||
assert len(res_trades) > 1200
|
||||
|
||||
def test_ccxt_get_fee(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||
exch, exchangename = exchange
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.persistence.trade_model import Trade
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_trade_fromjson():
|
||||
"""Test the Trade.from_json() method."""
|
||||
trade_string = """{
|
||||
|
@ -168,14 +171,17 @@ def test_trade_fromjson():
|
|||
]
|
||||
}"""
|
||||
trade = Trade.from_json(trade_string)
|
||||
Trade.session.add(trade)
|
||||
Trade.commit()
|
||||
|
||||
assert trade.id == 25
|
||||
assert trade.pair == 'ETH/USDT'
|
||||
assert trade.open_date == datetime(2022, 10, 18, 9, 12, 42, tzinfo=timezone.utc)
|
||||
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]
|
||||
assert last_o.order_filled_date == datetime(2022, 10, 18, 9, 45, 22, tzinfo=timezone.utc)
|
||||
assert last_o.order_filled_utc == datetime(2022, 10, 18, 9, 45, 22, tzinfo=timezone.utc)
|
||||
assert isinstance(last_o.order_date, datetime)
|
||||
|
|
|
@ -1200,6 +1200,10 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo
|
|||
"[{'ProducerPairList': 'ProducerPairList - default'}]",
|
||||
None
|
||||
),
|
||||
({"method": "RemotePairList", "number_assets": 10, "pairlist_url": "https://example.com"},
|
||||
"[{'RemotePairList': 'RemotePairList - 10 pairs from RemotePairlist.'}]",
|
||||
None
|
||||
),
|
||||
])
|
||||
def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig,
|
||||
desc_expected, exception_expected):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import json
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
@ -7,7 +7,7 @@ import requests
|
|||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.plugins.pairlist.RemotePairList import RemotePairList
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
from tests.conftest import get_patched_exchange, get_patched_freqtradebot, log_has
|
||||
from tests.conftest import EXMS, get_patched_exchange, get_patched_freqtradebot, log_has
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
|
@ -16,11 +16,12 @@ def rpl_config(default_conf):
|
|||
|
||||
default_conf['exchange']['pair_whitelist'] = [
|
||||
'ETH/USDT',
|
||||
'BTC/USDT',
|
||||
'XRP/USDT',
|
||||
]
|
||||
default_conf['exchange']['pair_blacklist'] = [
|
||||
'BLK/USDT'
|
||||
]
|
||||
|
||||
return default_conf
|
||||
|
||||
|
||||
|
@ -34,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'] = [
|
||||
{
|
||||
|
@ -183,3 +184,159 @@ def test_fetch_pairlist_mock_response_valid(mocker, rpl_config):
|
|||
assert pairs == ["ETH/USDT", "XRP/USDT", "LTC/USDT", "EOS/USDT"]
|
||||
assert time_elapsed == 0.4
|
||||
assert remote_pairlist._refresh_period == 60
|
||||
|
||||
|
||||
def test_remote_pairlist_init_wrong_mode(mocker, rpl_config):
|
||||
rpl_config['pairlists'] = [
|
||||
{
|
||||
"method": "RemotePairList",
|
||||
"mode": "blacklis",
|
||||
"number_assets": 20,
|
||||
"pairlist_url": "http://example.com/pairlist",
|
||||
"keep_pairlist_on_failure": True,
|
||||
}
|
||||
]
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r'`mode` not configured correctly. Supported Modes are "whitelist","blacklist"'
|
||||
):
|
||||
get_patched_freqtradebot(mocker, rpl_config)
|
||||
|
||||
rpl_config['pairlists'] = [
|
||||
{
|
||||
"method": "RemotePairList",
|
||||
"mode": "blacklist",
|
||||
"number_assets": 20,
|
||||
"pairlist_url": "http://example.com/pairlist",
|
||||
"keep_pairlist_on_failure": True,
|
||||
}
|
||||
]
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r'A `blacklist` mode RemotePairList can not be.*first.*'
|
||||
):
|
||||
get_patched_freqtradebot(mocker, rpl_config)
|
||||
|
||||
|
||||
def test_remote_pairlist_init_wrong_proc_mode(mocker, rpl_config):
|
||||
rpl_config['pairlists'] = [
|
||||
{
|
||||
"method": "RemotePairList",
|
||||
"processing_mode": "filler",
|
||||
"mode": "whitelist",
|
||||
"number_assets": 20,
|
||||
"pairlist_url": "http://example.com/pairlist",
|
||||
"keep_pairlist_on_failure": True,
|
||||
}
|
||||
]
|
||||
|
||||
get_patched_exchange(mocker, rpl_config)
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r'`processing_mode` not configured correctly. Supported Modes are "filter","append"'
|
||||
):
|
||||
get_patched_freqtradebot(mocker, rpl_config)
|
||||
|
||||
|
||||
def test_remote_pairlist_blacklist(mocker, rpl_config, caplog, markets, tickers):
|
||||
|
||||
mock_response = MagicMock()
|
||||
|
||||
mock_response.json.return_value = {
|
||||
"pairs": ["XRP/USDT"],
|
||||
"refresh_period": 60
|
||||
}
|
||||
|
||||
mock_response.headers = {
|
||||
"content-type": "application/json"
|
||||
}
|
||||
|
||||
rpl_config['pairlists'] = [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
},
|
||||
{
|
||||
"method": "RemotePairList",
|
||||
"mode": "blacklist",
|
||||
"pairlist_url": "http://example.com/pairlist",
|
||||
"number_assets": 3
|
||||
}
|
||||
]
|
||||
|
||||
mocker.patch.multiple(EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers
|
||||
)
|
||||
|
||||
mocker.patch("freqtrade.plugins.pairlist.RemotePairList.requests.get",
|
||||
return_value=mock_response)
|
||||
|
||||
exchange = get_patched_exchange(mocker, rpl_config)
|
||||
|
||||
pairlistmanager = PairListManager(exchange, rpl_config)
|
||||
|
||||
remote_pairlist = RemotePairList(exchange, pairlistmanager, rpl_config,
|
||||
rpl_config["pairlists"][1], 1)
|
||||
|
||||
pairs, time_elapsed = remote_pairlist.fetch_pairlist()
|
||||
|
||||
assert pairs == ["XRP/USDT"]
|
||||
|
||||
whitelist = remote_pairlist.filter_pairlist(rpl_config['exchange']['pair_whitelist'], {})
|
||||
assert whitelist == ["ETH/USDT"]
|
||||
|
||||
assert log_has(f"Blacklist - Filtered out pairs: {pairs}", caplog)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("processing_mode", ["filter", "append"])
|
||||
def test_remote_pairlist_whitelist(mocker, rpl_config, processing_mode, markets, tickers):
|
||||
|
||||
mock_response = MagicMock()
|
||||
|
||||
mock_response.json.return_value = {
|
||||
"pairs": ["XRP/USDT"],
|
||||
"refresh_period": 60
|
||||
}
|
||||
|
||||
mock_response.headers = {
|
||||
"content-type": "application/json"
|
||||
}
|
||||
|
||||
rpl_config['pairlists'] = [
|
||||
{
|
||||
"method": "StaticPairList",
|
||||
},
|
||||
{
|
||||
"method": "RemotePairList",
|
||||
"mode": "whitelist",
|
||||
"processing_mode": processing_mode,
|
||||
"pairlist_url": "http://example.com/pairlist",
|
||||
"number_assets": 3
|
||||
}
|
||||
]
|
||||
|
||||
mocker.patch.multiple(EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers
|
||||
)
|
||||
|
||||
mocker.patch("freqtrade.plugins.pairlist.RemotePairList.requests.get",
|
||||
return_value=mock_response)
|
||||
|
||||
exchange = get_patched_exchange(mocker, rpl_config)
|
||||
|
||||
pairlistmanager = PairListManager(exchange, rpl_config)
|
||||
|
||||
remote_pairlist = RemotePairList(exchange, pairlistmanager, rpl_config,
|
||||
rpl_config["pairlists"][1], 1)
|
||||
|
||||
pairs, time_elapsed = remote_pairlist.fetch_pairlist()
|
||||
|
||||
assert pairs == ["XRP/USDT"]
|
||||
|
||||
whitelist = remote_pairlist.filter_pairlist(rpl_config['exchange']['pair_whitelist'], {})
|
||||
assert whitelist == (["XRP/USDT"] if processing_mode == "filter" else ['ETH/USDT', 'XRP/USDT'])
|
||||
|
|
|
@ -97,7 +97,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
|
||||
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
||||
'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
|
||||
'remaining': ANY, 'status': ANY, 'ft_is_entry': True,
|
||||
'remaining': ANY, 'status': ANY, 'ft_is_entry': True, 'ft_fee_base': None,
|
||||
}],
|
||||
}
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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", [
|
||||
|
|
|
@ -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
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user