Merge branch 'freqtrade:develop' into develop
This commit is contained in:
commit
47764d52e5
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -360,6 +360,8 @@ jobs:
|
|||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
- name: Tests incl. ccxt compatibility tests
|
- name: Tests incl. ccxt compatibility tests
|
||||||
|
env:
|
||||||
|
CI_WEB_PROXY: http://152.67.78.211:13128
|
||||||
run: |
|
run: |
|
||||||
pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun
|
pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun
|
||||||
|
|
||||||
|
@ -8,16 +8,16 @@ repos:
|
|||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: "v0.942"
|
rev: "v0.991"
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
exclude: build_helpers
|
exclude: build_helpers
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.2.1
|
- types-cachetools==5.2.1
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.28.11.7
|
- types-requests==2.28.11.8
|
||||||
- types-tabulate==0.9.0.0
|
- types-tabulate==0.9.0.0
|
||||||
- types-python-dateutil==2.8.19.5
|
- types-python-dateutil==2.8.19.6
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
|
@ -70,20 +70,21 @@ docker push ${CACHE_IMAGE}:$TAG_ARM
|
|||||||
# Otherwise installation might fail.
|
# Otherwise installation might fail.
|
||||||
echo "create manifests"
|
echo "create manifests"
|
||||||
|
|
||||||
docker manifest create --amend ${IMAGE_NAME}:${TAG} ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
|
docker manifest create ${IMAGE_NAME}:${TAG} ${CACHE_IMAGE}:${TAG} ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI}
|
||||||
docker manifest push -p ${IMAGE_NAME}:${TAG}
|
docker manifest push -p ${IMAGE_NAME}:${TAG}
|
||||||
|
|
||||||
docker manifest create ${IMAGE_NAME}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT_ARM} ${CACHE_IMAGE}:${TAG_PLOT}
|
docker manifest create ${IMAGE_NAME}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT_ARM}
|
||||||
docker manifest push -p ${IMAGE_NAME}:${TAG_PLOT}
|
docker manifest push -p ${IMAGE_NAME}:${TAG_PLOT}
|
||||||
|
|
||||||
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI_ARM} ${CACHE_IMAGE}:${TAG_FREQAI}
|
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI_ARM}
|
||||||
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI}
|
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI}
|
||||||
|
|
||||||
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} ${CACHE_IMAGE}:${TAG_FREQAI_RL}
|
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM}
|
||||||
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL}
|
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL}
|
||||||
|
|
||||||
# Tag as latest for develop builds
|
# Tag as latest for develop builds
|
||||||
if [ "${TAG}" = "develop" ]; then
|
if [ "${TAG}" = "develop" ]; then
|
||||||
|
echo 'Tagging image as latest'
|
||||||
docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
|
docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
|
||||||
docker manifest push -p ${IMAGE_NAME}:latest
|
docker manifest push -p ${IMAGE_NAME}:latest
|
||||||
fi
|
fi
|
||||||
|
@ -26,7 +26,10 @@ if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then
|
|||||||
--cache-to=type=registry,ref=${CACHE_TAG} \
|
--cache-to=type=registry,ref=${CACHE_TAG} \
|
||||||
-f docker/Dockerfile.armhf \
|
-f docker/Dockerfile.armhf \
|
||||||
--platform ${PI_PLATFORM} \
|
--platform ${PI_PLATFORM} \
|
||||||
-t ${IMAGE_NAME}:${TAG_PI} --push .
|
-t ${IMAGE_NAME}:${TAG_PI} \
|
||||||
|
--push \
|
||||||
|
--provenance=false \
|
||||||
|
.
|
||||||
else
|
else
|
||||||
echo "event ${GITHUB_EVENT_NAME}: building with cache"
|
echo "event ${GITHUB_EVENT_NAME}: building with cache"
|
||||||
# Build regular image
|
# Build regular image
|
||||||
@ -35,12 +38,16 @@ else
|
|||||||
|
|
||||||
# Pull last build to avoid rebuilding the whole image
|
# Pull last build to avoid rebuilding the whole image
|
||||||
# docker pull --platform ${PI_PLATFORM} ${IMAGE_NAME}:${TAG}
|
# docker pull --platform ${PI_PLATFORM} ${IMAGE_NAME}:${TAG}
|
||||||
|
# disable provenance due to https://github.com/docker/buildx/issues/1509
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--cache-from=type=registry,ref=${CACHE_TAG} \
|
--cache-from=type=registry,ref=${CACHE_TAG} \
|
||||||
--cache-to=type=registry,ref=${CACHE_TAG} \
|
--cache-to=type=registry,ref=${CACHE_TAG} \
|
||||||
-f docker/Dockerfile.armhf \
|
-f docker/Dockerfile.armhf \
|
||||||
--platform ${PI_PLATFORM} \
|
--platform ${PI_PLATFORM} \
|
||||||
-t ${IMAGE_NAME}:${TAG_PI} --push .
|
-t ${IMAGE_NAME}:${TAG_PI} \
|
||||||
|
--push \
|
||||||
|
--provenance=false \
|
||||||
|
.
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
@ -68,12 +75,10 @@ fi
|
|||||||
|
|
||||||
docker images
|
docker images
|
||||||
|
|
||||||
docker push ${CACHE_IMAGE}
|
docker push ${CACHE_IMAGE}:$TAG
|
||||||
docker push ${CACHE_IMAGE}:$TAG_PLOT
|
docker push ${CACHE_IMAGE}:$TAG_PLOT
|
||||||
docker push ${CACHE_IMAGE}:$TAG_FREQAI
|
docker push ${CACHE_IMAGE}:$TAG_FREQAI
|
||||||
docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL
|
docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL
|
||||||
docker push ${CACHE_IMAGE}:$TAG
|
|
||||||
|
|
||||||
|
|
||||||
docker images
|
docker images
|
||||||
|
|
||||||
|
@ -59,20 +59,6 @@
|
|||||||
"pairlists": [
|
"pairlists": [
|
||||||
{"method": "StaticPairList"}
|
{"method": "StaticPairList"}
|
||||||
],
|
],
|
||||||
"edge": {
|
|
||||||
"enabled": false,
|
|
||||||
"process_throttle_secs": 3600,
|
|
||||||
"calculate_since_number_of_days": 7,
|
|
||||||
"allowed_risk": 0.01,
|
|
||||||
"stoploss_range_min": -0.01,
|
|
||||||
"stoploss_range_max": -0.1,
|
|
||||||
"stoploss_range_step": -0.01,
|
|
||||||
"minimum_winrate": 0.60,
|
|
||||||
"minimum_expectancy": 0.20,
|
|
||||||
"min_trade_number": 10,
|
|
||||||
"max_trade_duration_minute": 1440,
|
|
||||||
"remove_pumps": false
|
|
||||||
},
|
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"token": "your_telegram_token",
|
"token": "your_telegram_token",
|
||||||
|
@ -56,20 +56,6 @@
|
|||||||
"pairlists": [
|
"pairlists": [
|
||||||
{"method": "StaticPairList"}
|
{"method": "StaticPairList"}
|
||||||
],
|
],
|
||||||
"edge": {
|
|
||||||
"enabled": false,
|
|
||||||
"process_throttle_secs": 3600,
|
|
||||||
"calculate_since_number_of_days": 7,
|
|
||||||
"allowed_risk": 0.01,
|
|
||||||
"stoploss_range_min": -0.01,
|
|
||||||
"stoploss_range_max": -0.1,
|
|
||||||
"stoploss_range_step": -0.01,
|
|
||||||
"minimum_winrate": 0.60,
|
|
||||||
"minimum_expectancy": 0.20,
|
|
||||||
"min_trade_number": 10,
|
|
||||||
"max_trade_duration_minute": 1440,
|
|
||||||
"remove_pumps": false
|
|
||||||
},
|
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"token": "your_telegram_token",
|
"token": "your_telegram_token",
|
||||||
|
@ -21,8 +21,8 @@
|
|||||||
"ccxt_config": {},
|
"ccxt_config": {},
|
||||||
"ccxt_async_config": {},
|
"ccxt_async_config": {},
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"1INCH/USDT",
|
"1INCH/USDT:USDT",
|
||||||
"ALGO/USDT"
|
"ALGO/USDT:USDT"
|
||||||
],
|
],
|
||||||
"pair_blacklist": []
|
"pair_blacklist": []
|
||||||
},
|
},
|
||||||
@ -60,8 +60,8 @@
|
|||||||
"1h"
|
"1h"
|
||||||
],
|
],
|
||||||
"include_corr_pairlist": [
|
"include_corr_pairlist": [
|
||||||
"BTC/USDT",
|
"BTC/USDT:USDT",
|
||||||
"ETH/USDT"
|
"ETH/USDT:USDT"
|
||||||
],
|
],
|
||||||
"label_period_candles": 20,
|
"label_period_candles": 20,
|
||||||
"include_shifted_candles": 2,
|
"include_shifted_candles": 2,
|
||||||
|
@ -64,20 +64,6 @@
|
|||||||
"pairlists": [
|
"pairlists": [
|
||||||
{"method": "StaticPairList"}
|
{"method": "StaticPairList"}
|
||||||
],
|
],
|
||||||
"edge": {
|
|
||||||
"enabled": false,
|
|
||||||
"process_throttle_secs": 3600,
|
|
||||||
"calculate_since_number_of_days": 7,
|
|
||||||
"allowed_risk": 0.01,
|
|
||||||
"stoploss_range_min": -0.01,
|
|
||||||
"stoploss_range_max": -0.1,
|
|
||||||
"stoploss_range_step": -0.01,
|
|
||||||
"minimum_winrate": 0.60,
|
|
||||||
"minimum_expectancy": 0.20,
|
|
||||||
"min_trade_number": 10,
|
|
||||||
"max_trade_duration_minute": 1440,
|
|
||||||
"remove_pumps": false
|
|
||||||
},
|
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"token": "your_telegram_token",
|
"token": "your_telegram_token",
|
||||||
|
@ -32,7 +32,7 @@ To analyze the entry/exit tags, we now need to use the `freqtrade backtesting-an
|
|||||||
with `--analysis-groups` option provided with space-separated arguments (default `0 1 2`):
|
with `--analysis-groups` option provided with space-separated arguments (default `0 1 2`):
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 1 2 3 4
|
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 1 2 3 4 5
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will read from the last backtesting results. The `--analysis-groups` option is
|
This command will read from the last backtesting results. The `--analysis-groups` option is
|
||||||
@ -43,6 +43,7 @@ ranging from the simplest (0) to the most detailed per pair, per buy and per sel
|
|||||||
* 2: profit summaries grouped by enter_tag and exit_tag
|
* 2: profit summaries grouped by enter_tag and exit_tag
|
||||||
* 3: profit summaries grouped by pair and enter_tag
|
* 3: profit summaries grouped by pair and enter_tag
|
||||||
* 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
|
* 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
|
||||||
|
* 5: profit summaries grouped by exit_tag
|
||||||
|
|
||||||
More options are available by running with the `-h` option.
|
More options are available by running with the `-h` option.
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ This function needs to return a floating point number (`float`). Smaller numbers
|
|||||||
|
|
||||||
## Overriding pre-defined spaces
|
## Overriding pre-defined spaces
|
||||||
|
|
||||||
To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_space`, `trailing_space`), define a nested class called Hyperopt and define the required spaces as follows:
|
To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_space`, `trailing_space`, `max_open_trades_space`), define a nested class called Hyperopt and define the required spaces as follows:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal
|
from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal
|
||||||
@ -123,6 +123,12 @@ class MyAwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
Categorical([True, False], name='trailing_only_offset_is_reached'),
|
Categorical([True, False], name='trailing_only_offset_is_reached'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Define a custom max_open_trades space
|
||||||
|
def max_open_trades_space(self) -> List[Dimension]:
|
||||||
|
return [
|
||||||
|
Integer(-1, 10, name='max_open_trades'),
|
||||||
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
|
@ -75,3 +75,7 @@ This loop will be repeated again and again until the bot is stopped.
|
|||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Both Backtesting and Hyperopt include exchange default Fees in the calculation. Custom fees can be passed to backtesting / hyperopt by specifying the `--fee` argument.
|
Both Backtesting and Hyperopt include exchange default Fees in the calculation. Custom fees can be passed to backtesting / hyperopt by specifying the `--fee` argument.
|
||||||
|
|
||||||
|
!!! Warning "Callback call frequency"
|
||||||
|
Backtesting will call each callback at max. once per candle (`--timeframe-detail` modifies this behavior to once per detailed candle).
|
||||||
|
Most callbacks will be called once per iteration in live (usually every ~5s) - which can cause backtesting mismatches.
|
||||||
|
@ -134,7 +134,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
|------------|-------------|
|
|------------|-------------|
|
||||||
| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation that can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade).<br> **Datatype:** Positive integer or -1.
|
| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation that can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Positive integer or -1.
|
||||||
| `stake_currency` | **Required.** Crypto-currency used for trading. <br> **Datatype:** String
|
| `stake_currency` | **Required.** Crypto-currency used for trading. <br> **Datatype:** String
|
||||||
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float or `"unlimited"`.
|
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float or `"unlimited"`.
|
||||||
| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`.
|
| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`.
|
||||||
@ -263,6 +263,7 @@ Values set in the configuration file always overwrite values set in the strategy
|
|||||||
* `minimal_roi`
|
* `minimal_roi`
|
||||||
* `timeframe`
|
* `timeframe`
|
||||||
* `stoploss`
|
* `stoploss`
|
||||||
|
* `max_open_trades`
|
||||||
* `trailing_stop`
|
* `trailing_stop`
|
||||||
* `trailing_stop_positive`
|
* `trailing_stop_positive`
|
||||||
* `trailing_stop_positive_offset`
|
* `trailing_stop_positive_offset`
|
||||||
|
@ -75,6 +75,25 @@ Binance has been split into 2, and users must use the correct ccxt exchange ID f
|
|||||||
* [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`.
|
* [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`.
|
||||||
* [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`.
|
* [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`.
|
||||||
|
|
||||||
|
### Binance RSA keys
|
||||||
|
|
||||||
|
Freqtrade supports binance RSA API keys.
|
||||||
|
|
||||||
|
We recommend to use them as environment variable.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
export FREQTRADE__EXCHANGE__SECRET="$(cat ./rsa_binance.private)"
|
||||||
|
```
|
||||||
|
|
||||||
|
They can however also be configured via configuration file. Since json doesn't support multi-line strings, you'll have to replace all newlines with `\n` to have a valid json file.
|
||||||
|
|
||||||
|
``` json
|
||||||
|
// ...
|
||||||
|
"key": "<someapikey>",
|
||||||
|
"secret": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBABACAFQA<...>s8KX8=\n-----END PRIVATE KEY-----"
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
### Binance Futures
|
### Binance Futures
|
||||||
|
|
||||||
Binance has specific (unfortunately complex) [Futures Trading Quantitative Rules](https://www.binance.com/en/support/faq/4f462ebe6ff445d4a170be7d9e897272) which need to be followed, and which prohibit a too low stake-amount (among others) for too many orders.
|
Binance has specific (unfortunately complex) [Futures Trading Quantitative Rules](https://www.binance.com/en/support/faq/4f462ebe6ff445d4a170be7d9e897272) which need to be followed, and which prohibit a too low stake-amount (among others) for too many orders.
|
||||||
|
@ -34,7 +34,7 @@ Setting up and running a Reinforcement Learning model is the same as running a R
|
|||||||
freqtrade trade --freqaimodel ReinforcementLearner --strategy MyRLStrategy --config config.json
|
freqtrade trade --freqaimodel ReinforcementLearner --strategy MyRLStrategy --config config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
where `ReinforcementLearner` will use the templated `ReinforcementLearner` from `freqai/prediction_models/ReinforcementLearner` (or a custom user defined one located in `user_data/freqaimodels`). The strategy, on the other hand, follows the same base [feature engineering](freqai-feature-engineering.md) with `feature_engineering_*` as a typical Regressor. The difference lies in the creation of the targets, Reinforcement Learning doesnt require them. However, FreqAI requires a default (neutral) value to be set in the action column:
|
where `ReinforcementLearner` will use the templated `ReinforcementLearner` from `freqai/prediction_models/ReinforcementLearner` (or a custom user defined one located in `user_data/freqaimodels`). The strategy, on the other hand, follows the same base [feature engineering](freqai-feature-engineering.md) with `feature_engineering_*` as a typical Regressor. The difference lies in the creation of the targets, Reinforcement Learning doesn't require them. However, FreqAI requires a default (neutral) value to be set in the action column:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def set_freqai_targets(self, dataframe, **kwargs):
|
def set_freqai_targets(self, dataframe, **kwargs):
|
||||||
@ -52,18 +52,18 @@ where `ReinforcementLearner` will use the templated `ReinforcementLearner` from
|
|||||||
"""
|
"""
|
||||||
# For RL, there are no direct targets to set. This is filler (neutral)
|
# For RL, there are no direct targets to set. This is filler (neutral)
|
||||||
# until the agent sends an action.
|
# until the agent sends an action.
|
||||||
df["&-action"] = 0
|
dataframe["&-action"] = 0
|
||||||
```
|
```
|
||||||
|
|
||||||
Most of the function remains the same as for typical Regressors, however, the function above shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment:
|
Most of the function remains the same as for typical Regressors, however, the function above shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def feature_engineering_standard():
|
def feature_engineering_standard(self, dataframe, **kwargs):
|
||||||
# The following features are necessary for RL models
|
# The following features are necessary for RL models
|
||||||
informative[f"%-raw_close"] = informative["close"]
|
dataframe[f"%-raw_close"] = dataframe["close"]
|
||||||
informative[f"%-raw_open"] = informative["open"]
|
dataframe[f"%-raw_open"] = dataframe["open"]
|
||||||
informative[f"%-raw_high"] = informative["high"]
|
dataframe[f"%-raw_high"] = dataframe["high"]
|
||||||
informative[f"%-raw_low"] = informative["low"]
|
dataframe[f"%-raw_low"] = dataframe["low"]
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, there is no explicit "label" to make - instead it is necessary to assign the `&-action` column which will contain the agent's actions when accessed in `populate_entry/exit_trends()`. In the present example, the neutral action to 0. This value should align with the environment used. FreqAI provides two environments, both use 0 as the neutral action.
|
Finally, there is no explicit "label" to make - instead it is necessary to assign the `&-action` column which will contain the agent's actions when accessed in `populate_entry/exit_trends()`. In the present example, the neutral action to 0. This value should align with the environment used. FreqAI provides two environments, both use 0 as the neutral action.
|
||||||
@ -243,7 +243,6 @@ FreqAI also provides a built in episodic summary logger called `self.tensorboard
|
|||||||
!!! Note
|
!!! Note
|
||||||
The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)` would add 0.23 to `float_metric`. In this case you can also disable incrementing using `inc=False` parameter.
|
The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)` would add 0.23 to `float_metric`. In this case you can also disable incrementing using `inc=False` parameter.
|
||||||
|
|
||||||
|
|
||||||
### Choosing a base environment
|
### Choosing a base environment
|
||||||
|
|
||||||
FreqAI provides three base environments, `Base3ActionRLEnvironment`, `Base4ActionEnvironment` and `Base5ActionEnvironment`. As the names imply, the environments are customized for agents that can select from 3, 4 or 5 actions. The `Base3ActionEnvironment` is the simplest, the agent can select from hold, long, or short. This environment can also be used for long-only bots (it automatically follows the `can_short` flag from the strategy), where long is the enter condition and short is the exit condition. Meanwhile, in the `Base4ActionEnvironment`, the agent can enter long, enter short, hold neutral, or exit position. Finally, in the `Base5ActionEnvironment`, the agent has the same actions as Base4, but instead of a single exit action, it separates exit long and exit short. The main changes stemming from the environment selection include:
|
FreqAI provides three base environments, `Base3ActionRLEnvironment`, `Base4ActionEnvironment` and `Base5ActionEnvironment`. As the names imply, the environments are customized for agents that can select from 3, 4 or 5 actions. The `Base3ActionEnvironment` is the simplest, the agent can select from hold, long, or short. This environment can also be used for long-only bots (it automatically follows the `can_short` flag from the strategy), where long is the enter condition and short is the exit condition. Meanwhile, in the `Base4ActionEnvironment`, the agent can enter long, enter short, hold neutral, or exit position. Finally, in the `Base5ActionEnvironment`, the agent has the same actions as Base4, but instead of a single exit action, it separates exit long and exit short. The main changes stemming from the environment selection include:
|
||||||
|
@ -50,7 +50,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
|||||||
[--eps] [--dmmp] [--enable-protections]
|
[--eps] [--dmmp] [--enable-protections]
|
||||||
[--dry-run-wallet DRY_RUN_WALLET]
|
[--dry-run-wallet DRY_RUN_WALLET]
|
||||||
[--timeframe-detail TIMEFRAME_DETAIL] [-e INT]
|
[--timeframe-detail TIMEFRAME_DETAIL] [-e INT]
|
||||||
[--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]]
|
[--spaces {all,buy,sell,roi,stoploss,trailing,protection,trades,default} [{all,buy,sell,roi,stoploss,trailing,protection,trades,default} ...]]
|
||||||
[--print-all] [--no-color] [--print-json] [-j JOBS]
|
[--print-all] [--no-color] [--print-json] [-j JOBS]
|
||||||
[--random-state INT] [--min-trades INT]
|
[--random-state INT] [--min-trades INT]
|
||||||
[--hyperopt-loss NAME] [--disable-param-export]
|
[--hyperopt-loss NAME] [--disable-param-export]
|
||||||
@ -96,7 +96,7 @@ optional arguments:
|
|||||||
Specify detail timeframe for backtesting (`1m`, `5m`,
|
Specify detail timeframe for backtesting (`1m`, `5m`,
|
||||||
`30m`, `1h`, `1d`).
|
`30m`, `1h`, `1d`).
|
||||||
-e INT, --epochs INT Specify number of epochs (default: 100).
|
-e INT, --epochs INT Specify number of epochs (default: 100).
|
||||||
--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]
|
--spaces {all,buy,sell,roi,stoploss,trailing,protection,trades,default} [{all,buy,sell,roi,stoploss,trailing,protection,trades,default} ...]
|
||||||
Specify which parameters to hyperopt. Space-separated
|
Specify which parameters to hyperopt. Space-separated
|
||||||
list.
|
list.
|
||||||
--print-all Print all results, not only the best ones.
|
--print-all Print all results, not only the best ones.
|
||||||
@ -180,6 +180,7 @@ Rarely you may also need to create a [nested class](advanced-hyperopt.md#overrid
|
|||||||
* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps)
|
* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps)
|
||||||
* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default)
|
* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default)
|
||||||
* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default)
|
* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default)
|
||||||
|
* `max_open_trades_space` - for custom max_open_trades optimization (if you need the ranges for the max_open_trades parameter in the optimization hyperspace that differ from default)
|
||||||
|
|
||||||
!!! Tip "Quickly optimize ROI, stoploss and trailing stoploss"
|
!!! Tip "Quickly optimize ROI, stoploss and trailing stoploss"
|
||||||
You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything in your strategy.
|
You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything in your strategy.
|
||||||
@ -643,6 +644,7 @@ Legal values are:
|
|||||||
* `roi`: just optimize the minimal profit table for your strategy
|
* `roi`: just optimize the minimal profit table for your strategy
|
||||||
* `stoploss`: search for the best stoploss value
|
* `stoploss`: search for the best stoploss value
|
||||||
* `trailing`: search for the best trailing stop values
|
* `trailing`: search for the best trailing stop values
|
||||||
|
* `trades`: search for the best max open trades values
|
||||||
* `protection`: search for the best protection parameters (read the [protections section](#optimizing-protections) on how to properly define these)
|
* `protection`: search for the best protection parameters (read the [protections section](#optimizing-protections) on how to properly define these)
|
||||||
* `default`: `all` except `trailing` and `protection`
|
* `default`: `all` except `trailing` and `protection`
|
||||||
* space-separated list of any of the above values for example `--spaces roi stoploss`
|
* space-separated list of any of the above values for example `--spaces roi stoploss`
|
||||||
@ -916,5 +918,5 @@ Once the optimized strategy has been implemented into your strategy, you should
|
|||||||
To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
|
To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
|
||||||
|
|
||||||
Should results not match, please double-check to make sure you transferred all conditions correctly.
|
Should results not match, please double-check to make sure you transferred all conditions correctly.
|
||||||
Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy.
|
Pay special care to the stoploss, max_open_trades and trailing stoploss parameters, as these are often set in configuration files, which override changes to the strategy.
|
||||||
You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`).
|
You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss`, `max_open_trades` or `trailing_stop`).
|
||||||
|
@ -67,8 +67,6 @@ You will also have to pick a "margin mode" (explanation below) - with freqtrade
|
|||||||
Freqtrade follows the [ccxt naming conventions for futures](https://docs.ccxt.com/en/latest/manual.html?#perpetual-swap-perpetual-future).
|
Freqtrade follows the [ccxt naming conventions for futures](https://docs.ccxt.com/en/latest/manual.html?#perpetual-swap-perpetual-future).
|
||||||
A futures pair will therefore have the naming of `base/quote:settle` (e.g. `ETH/USDT:USDT`).
|
A futures pair will therefore have the naming of `base/quote:settle` (e.g. `ETH/USDT:USDT`).
|
||||||
|
|
||||||
Binance is currently still an exception to this naming scheme, where pairs are named `ETH/USDT` also for futures markets, but will be aligned as soon as CCXT is ready.
|
|
||||||
|
|
||||||
### Margin mode
|
### Margin mode
|
||||||
|
|
||||||
On top of `trading_mode` - you will also have to configure your `margin_mode`.
|
On top of `trading_mode` - you will also have to configure your `margin_mode`.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
markdown==3.3.7
|
markdown==3.3.7
|
||||||
mkdocs==1.4.2
|
mkdocs==1.4.2
|
||||||
mkdocs-material==9.0.3
|
mkdocs-material==9.0.5
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==9.9
|
pymdown-extensions==9.9.1
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
|
@ -659,6 +659,7 @@ Position adjustments will always be applied in the direction of the trade, so a
|
|||||||
|
|
||||||
!!! Warning "Backtesting"
|
!!! Warning "Backtesting"
|
||||||
During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected.
|
During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected.
|
||||||
|
This can also cause deviating results between live and backtesting, since backtesting can adjust the trade only once per candle, whereas live could adjust the trade multiple times per candle.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
@ -827,7 +828,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
# Limit orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
|
# Limit orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
|
||||||
if pair == 'BTC/USDT' and entry_tag == 'long_sma200' and side == 'long' and (current_time - timedelta(minutes=10) > trade.open_date_utc:
|
if pair == 'BTC/USDT' and entry_tag == 'long_sma200' and side == 'long' and (current_time - timedelta(minutes=10)) > trade.open_date_utc:
|
||||||
# just cancel the order if it has been filled more than half of the amount
|
# just cancel the order if it has been filled more than half of the amount
|
||||||
if order.filled > order.remaining:
|
if order.filled > order.remaining:
|
||||||
return None
|
return None
|
||||||
|
@ -251,7 +251,8 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"spaces": Arg(
|
"spaces": Arg(
|
||||||
'--spaces',
|
'--spaces',
|
||||||
help='Specify which parameters to hyperopt. Space-separated list.',
|
help='Specify which parameters to hyperopt. Space-separated list.',
|
||||||
choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'default'],
|
choices=['all', 'buy', 'sell', 'roi', 'stoploss',
|
||||||
|
'trailing', 'protection', 'trades', 'default'],
|
||||||
nargs='+',
|
nargs='+',
|
||||||
default='default',
|
default='default',
|
||||||
),
|
),
|
||||||
@ -632,10 +633,11 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"1: by enter_tag, "
|
"1: by enter_tag, "
|
||||||
"2: by enter_tag and exit_tag, "
|
"2: by enter_tag and exit_tag, "
|
||||||
"3: by pair and enter_tag, "
|
"3: by pair and enter_tag, "
|
||||||
"4: by pair, enter_ and exit_tag (this can get quite large)"),
|
"4: by pair, enter_ and exit_tag (this can get quite large), "
|
||||||
|
"5: by exit_tag"),
|
||||||
nargs='+',
|
nargs='+',
|
||||||
default=['0', '1', '2'],
|
default=['0', '1', '2'],
|
||||||
choices=['0', '1', '2', '3', '4'],
|
choices=['0', '1', '2', '3', '4', '5'],
|
||||||
),
|
),
|
||||||
"enter_reason_list": Arg(
|
"enter_reason_list": Arg(
|
||||||
"--enter-reason-list",
|
"--enter-reason-list",
|
||||||
|
@ -14,6 +14,7 @@ from freqtrade.exceptions import OperationalException
|
|||||||
from freqtrade.exchange import market_is_active, timeframe_to_minutes
|
from freqtrade.exchange import market_is_active, timeframe_to_minutes
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist
|
||||||
from freqtrade.resolvers import ExchangeResolver
|
from freqtrade.resolvers import ExchangeResolver
|
||||||
|
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -86,6 +87,7 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
|||||||
"Please use `--dl-trades` instead for this exchange "
|
"Please use `--dl-trades` instead for this exchange "
|
||||||
"(will unfortunately take a long time)."
|
"(will unfortunately take a long time)."
|
||||||
)
|
)
|
||||||
|
migrate_binance_futures_data(config)
|
||||||
pairs_not_available = refresh_backtest_ohlcv_data(
|
pairs_not_available = refresh_backtest_ohlcv_data(
|
||||||
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
|
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
|
||||||
datadir=config['datadir'], timerange=timerange,
|
datadir=config['datadir'], timerange=timerange,
|
||||||
@ -145,6 +147,7 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
|||||||
"""
|
"""
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
if ohlcv:
|
if ohlcv:
|
||||||
|
migrate_binance_futures_data(config)
|
||||||
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', ['spot'])]
|
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', ['spot'])]
|
||||||
for candle_type in candle_types:
|
for candle_type in candle_types:
|
||||||
convert_ohlcv_format(config,
|
convert_ohlcv_format(config,
|
||||||
|
@ -28,7 +28,7 @@ class Configuration:
|
|||||||
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
|
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args: Dict[str, Any], runmode: RunMode = None) -> None:
|
def __init__(self, args: Dict[str, Any], runmode: Optional[RunMode] = None) -> None:
|
||||||
self.args = args
|
self.args = args
|
||||||
self.config: Optional[Config] = None
|
self.config: Optional[Config] = None
|
||||||
self.runmode = runmode
|
self.runmode = runmode
|
||||||
|
@ -6,7 +6,7 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import rapidjson
|
import rapidjson
|
||||||
|
|
||||||
@ -75,7 +75,8 @@ def load_config_file(path: str) -> Dict[str, Any]:
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def load_from_files(files: List[str], base_path: Path = None, level: int = 0) -> Dict[str, Any]:
|
def load_from_files(
|
||||||
|
files: List[str], base_path: Optional[Path] = None, level: int = 0) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Recursively load configuration files if specified.
|
Recursively load configuration files if specified.
|
||||||
Sub-files are assumed to be relative to the initial config.
|
Sub-files are assumed to be relative to the initial config.
|
||||||
|
@ -636,7 +636,6 @@ SCHEMA_TRADE_REQUIRED = [
|
|||||||
|
|
||||||
SCHEMA_BACKTEST_REQUIRED = [
|
SCHEMA_BACKTEST_REQUIRED = [
|
||||||
'exchange',
|
'exchange',
|
||||||
'max_open_trades',
|
|
||||||
'stake_currency',
|
'stake_currency',
|
||||||
'stake_amount',
|
'stake_amount',
|
||||||
'dry_run_wallet',
|
'dry_run_wallet',
|
||||||
@ -646,6 +645,7 @@ SCHEMA_BACKTEST_REQUIRED = [
|
|||||||
SCHEMA_BACKTEST_REQUIRED_FINAL = SCHEMA_BACKTEST_REQUIRED + [
|
SCHEMA_BACKTEST_REQUIRED_FINAL = SCHEMA_BACKTEST_REQUIRED + [
|
||||||
'stoploss',
|
'stoploss',
|
||||||
'minimal_roi',
|
'minimal_roi',
|
||||||
|
'max_open_trades'
|
||||||
]
|
]
|
||||||
|
|
||||||
SCHEMA_MINIMAL_REQUIRED = [
|
SCHEMA_MINIMAL_REQUIRED = [
|
||||||
@ -681,3 +681,4 @@ MakerTaker = Literal['maker', 'taker']
|
|||||||
BidAsk = Literal['bid', 'ask']
|
BidAsk = Literal['bid', 'ask']
|
||||||
|
|
||||||
Config = Dict[str, Any]
|
Config = Dict[str, Any]
|
||||||
|
IntOrInf = float
|
||||||
|
@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional, Union
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade.constants import LAST_BT_RESULT_FN
|
from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import json_load
|
from freqtrade.misc import json_load
|
||||||
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
||||||
@ -90,7 +90,8 @@ def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str:
|
|||||||
return 'hyperopt_results.pickle'
|
return 'hyperopt_results.pickle'
|
||||||
|
|
||||||
|
|
||||||
def get_latest_hyperopt_file(directory: Union[Path, str], predef_filename: str = None) -> Path:
|
def get_latest_hyperopt_file(
|
||||||
|
directory: Union[Path, str], predef_filename: Optional[str] = None) -> Path:
|
||||||
"""
|
"""
|
||||||
Get latest hyperopt export based on '.last_result.json'.
|
Get latest hyperopt export based on '.last_result.json'.
|
||||||
:param directory: Directory to search for last result
|
:param directory: Directory to search for last result
|
||||||
@ -193,7 +194,7 @@ def get_backtest_resultlist(dirname: Path):
|
|||||||
|
|
||||||
|
|
||||||
def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str],
|
def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str],
|
||||||
min_backtest_date: datetime = None) -> Dict[str, Any]:
|
min_backtest_date: Optional[datetime] = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Find existing backtest stats that match specified run IDs and load them.
|
Find existing backtest stats that match specified run IDs and load them.
|
||||||
:param dirname: pathlib.Path object, or string pointing to the file.
|
:param dirname: pathlib.Path object, or string pointing to the file.
|
||||||
@ -332,7 +333,7 @@ def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataF
|
|||||||
|
|
||||||
|
|
||||||
def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
|
def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
|
||||||
max_open_trades: int) -> pd.DataFrame:
|
max_open_trades: IntOrInf) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Find overlapping trades by expanding each trade once per period it was open
|
Find overlapping trades by expanding each trade once per period it was open
|
||||||
and then counting overlaps
|
and then counting overlaps
|
||||||
|
@ -281,7 +281,7 @@ class DataProvider:
|
|||||||
def historic_ohlcv(
|
def historic_ohlcv(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
timeframe: str = None,
|
timeframe: Optional[str] = None,
|
||||||
candle_type: str = ''
|
candle_type: str = ''
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@ -333,7 +333,7 @@ class DataProvider:
|
|||||||
def get_pair_dataframe(
|
def get_pair_dataframe(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
timeframe: str = None,
|
timeframe: Optional[str] = None,
|
||||||
candle_type: str = ''
|
candle_type: str = ''
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@ -415,7 +415,7 @@ class DataProvider:
|
|||||||
|
|
||||||
def refresh(self,
|
def refresh(self,
|
||||||
pairlist: ListPairsWithTimeframes,
|
pairlist: ListPairsWithTimeframes,
|
||||||
helping_pairs: ListPairsWithTimeframes = None) -> None:
|
helping_pairs: Optional[ListPairsWithTimeframes] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Refresh data, called with each cycle
|
Refresh data, called with each cycle
|
||||||
"""
|
"""
|
||||||
@ -439,7 +439,7 @@ class DataProvider:
|
|||||||
def ohlcv(
|
def ohlcv(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
timeframe: str = None,
|
timeframe: Optional[str] = None,
|
||||||
copy: bool = True,
|
copy: bool = True,
|
||||||
candle_type: str = ''
|
candle_type: str = ''
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
|
@ -141,6 +141,12 @@ def _do_group_table_output(bigdf, glist):
|
|||||||
# 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
|
# 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
|
||||||
if g == "4":
|
if g == "4":
|
||||||
group_mask = ['pair', 'enter_reason', 'exit_reason']
|
group_mask = ['pair', 'enter_reason', 'exit_reason']
|
||||||
|
|
||||||
|
# 5: profit summaries grouped by exit_tag
|
||||||
|
if g == "5":
|
||||||
|
group_mask = ['exit_reason']
|
||||||
|
sortcols = ['exit_reason']
|
||||||
|
|
||||||
if group_mask:
|
if group_mask:
|
||||||
new = bigdf.groupby(group_mask).agg(agg_mask).reset_index()
|
new = bigdf.groupby(group_mask).agg(agg_mask).reset_index()
|
||||||
new.columns = group_mask + agg_cols
|
new.columns = group_mask + agg_cols
|
||||||
|
@ -28,8 +28,8 @@ def load_pair_history(pair: str,
|
|||||||
fill_up_missing: bool = True,
|
fill_up_missing: bool = True,
|
||||||
drop_incomplete: bool = False,
|
drop_incomplete: bool = False,
|
||||||
startup_candles: int = 0,
|
startup_candles: int = 0,
|
||||||
data_format: str = None,
|
data_format: Optional[str] = None,
|
||||||
data_handler: IDataHandler = None,
|
data_handler: Optional[IDataHandler] = None,
|
||||||
candle_type: CandleType = CandleType.SPOT
|
candle_type: CandleType = CandleType.SPOT
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@ -69,7 +69,7 @@ def load_data(datadir: Path,
|
|||||||
fail_without_data: bool = False,
|
fail_without_data: bool = False,
|
||||||
data_format: str = 'json',
|
data_format: str = 'json',
|
||||||
candle_type: CandleType = CandleType.SPOT,
|
candle_type: CandleType = CandleType.SPOT,
|
||||||
user_futures_funding_rate: int = None,
|
user_futures_funding_rate: Optional[int] = None,
|
||||||
) -> Dict[str, DataFrame]:
|
) -> Dict[str, DataFrame]:
|
||||||
"""
|
"""
|
||||||
Load ohlcv history data for a list of pairs.
|
Load ohlcv history data for a list of pairs.
|
||||||
@ -116,7 +116,7 @@ def refresh_data(*, datadir: Path,
|
|||||||
timeframe: str,
|
timeframe: str,
|
||||||
pairs: List[str],
|
pairs: List[str],
|
||||||
exchange: Exchange,
|
exchange: Exchange,
|
||||||
data_format: str = None,
|
data_format: Optional[str] = None,
|
||||||
timerange: Optional[TimeRange] = None,
|
timerange: Optional[TimeRange] = None,
|
||||||
candle_type: CandleType,
|
candle_type: CandleType,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -189,7 +189,7 @@ def _download_pair_history(pair: str, *,
|
|||||||
timeframe: str = '5m',
|
timeframe: str = '5m',
|
||||||
process: str = '',
|
process: str = '',
|
||||||
new_pairs_days: int = 30,
|
new_pairs_days: int = 30,
|
||||||
data_handler: IDataHandler = None,
|
data_handler: Optional[IDataHandler] = None,
|
||||||
timerange: Optional[TimeRange] = None,
|
timerange: Optional[TimeRange] = None,
|
||||||
candle_type: CandleType,
|
candle_type: CandleType,
|
||||||
erase: bool = False,
|
erase: bool = False,
|
||||||
@ -272,7 +272,7 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
|
|||||||
datadir: Path, trading_mode: str,
|
datadir: Path, trading_mode: str,
|
||||||
timerange: Optional[TimeRange] = None,
|
timerange: Optional[TimeRange] = None,
|
||||||
new_pairs_days: int = 30, erase: bool = False,
|
new_pairs_days: int = 30, erase: bool = False,
|
||||||
data_format: str = None,
|
data_format: Optional[str] = None,
|
||||||
prepend: bool = False,
|
prepend: bool = False,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""
|
"""
|
||||||
|
@ -374,6 +374,21 @@ class IDataHandler(ABC):
|
|||||||
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
|
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
|
||||||
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}")
|
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}")
|
||||||
|
|
||||||
|
def rename_futures_data(
|
||||||
|
self, pair: str, new_pair: str, timeframe: str, candle_type: CandleType):
|
||||||
|
"""
|
||||||
|
Temporary method to migrate data from old naming to new naming (BTC/USDT -> BTC/USDT:USDT)
|
||||||
|
Only used for binance to support the binance futures naming unification.
|
||||||
|
"""
|
||||||
|
|
||||||
|
file_old = self._pair_data_filename(self._datadir, pair, timeframe, candle_type)
|
||||||
|
file_new = self._pair_data_filename(self._datadir, new_pair, timeframe, candle_type)
|
||||||
|
# print(file_old, file_new)
|
||||||
|
if file_new.exists():
|
||||||
|
logger.warning(f"{file_new} exists already, can't migrate {pair}.")
|
||||||
|
return
|
||||||
|
file_old.rename(file_new)
|
||||||
|
|
||||||
|
|
||||||
def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
|
def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
|
||||||
"""
|
"""
|
||||||
@ -403,8 +418,8 @@ def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
|
|||||||
raise ValueError(f"No datahandler for datatype {datatype} available.")
|
raise ValueError(f"No datahandler for datatype {datatype} available.")
|
||||||
|
|
||||||
|
|
||||||
def get_datahandler(datadir: Path, data_format: str = None,
|
def get_datahandler(datadir: Path, data_format: Optional[str] = None,
|
||||||
data_handler: IDataHandler = None) -> IDataHandler:
|
data_handler: Optional[IDataHandler] = None) -> IDataHandler:
|
||||||
"""
|
"""
|
||||||
:param datadir: Folder to save data
|
:param datadir: Folder to save data
|
||||||
:param data_format: dataformat to use
|
:param data_format: dataformat to use
|
||||||
|
@ -28,7 +28,7 @@ class Binance(Exchange):
|
|||||||
"trades_pagination": "id",
|
"trades_pagination": "id",
|
||||||
"trades_pagination_arg": "fromId",
|
"trades_pagination_arg": "fromId",
|
||||||
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
|
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
|
||||||
"ccxt_futures_name": "future"
|
"ccxt_futures_name": "swap"
|
||||||
}
|
}
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,6 @@
|
|||||||
Cryptocurrency Exchanges support
|
Cryptocurrency Exchanges support
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import http
|
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
@ -45,12 +44,6 @@ from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# Workaround for adding samesite support to pre 3.8 python
|
|
||||||
# Only applies to python3.7, and only on certain exchanges (kraken)
|
|
||||||
# Replicates the fix from starlette (which is actually causing this problem)
|
|
||||||
http.cookies.Morsel._reserved["samesite"] = "SameSite" # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class Exchange:
|
class Exchange:
|
||||||
|
|
||||||
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
|
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
|
||||||
@ -682,7 +675,7 @@ class Exchange:
|
|||||||
f"Freqtrade does not support {mm_value} {trading_mode.value} on {self.name}"
|
f"Freqtrade does not support {mm_value} {trading_mode.value} on {self.name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_option(self, param: str, default: Any = None) -> Any:
|
def get_option(self, param: str, default: Optional[Any] = None) -> Any:
|
||||||
"""
|
"""
|
||||||
Get parameter value from _ft_has
|
Get parameter value from _ft_has
|
||||||
"""
|
"""
|
||||||
@ -1357,7 +1350,7 @@ class Exchange:
|
|||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def fetch_positions(self, pair: str = None) -> List[Dict]:
|
def fetch_positions(self, pair: Optional[str] = None) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Fetch positions from the exchange.
|
Fetch positions from the exchange.
|
||||||
If no pair is given, all positions are returned.
|
If no pair is given, all positions are returned.
|
||||||
@ -1801,7 +1794,7 @@ class Exchange:
|
|||||||
def get_historic_ohlcv(self, pair: str, timeframe: str,
|
def get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||||
since_ms: int, candle_type: CandleType,
|
since_ms: int, candle_type: CandleType,
|
||||||
is_new_pair: bool = False,
|
is_new_pair: bool = False,
|
||||||
until_ms: int = None) -> List:
|
until_ms: Optional[int] = None) -> List:
|
||||||
"""
|
"""
|
||||||
Get candle history using asyncio and returns the list of candles.
|
Get candle history using asyncio and returns the list of candles.
|
||||||
Handles all async work for this.
|
Handles all async work for this.
|
||||||
@ -2674,7 +2667,7 @@ class Exchange:
|
|||||||
:param amount: Trade amount
|
:param amount: Trade amount
|
||||||
:param open_date: Open date of the trade
|
:param open_date: Open date of the trade
|
||||||
:return: funding fee since open_date
|
:return: funding fee since open_date
|
||||||
:raies: ExchangeError if something goes wrong.
|
:raises: ExchangeError if something goes wrong.
|
||||||
"""
|
"""
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
|
@ -15,18 +15,19 @@ from freqtrade.util import FtPrecise
|
|||||||
CcxtModuleType = Any
|
CcxtModuleType = Any
|
||||||
|
|
||||||
|
|
||||||
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
|
def is_exchange_known_ccxt(
|
||||||
|
exchange_name: str, ccxt_module: Optional[CcxtModuleType] = None) -> bool:
|
||||||
return exchange_name in ccxt_exchanges(ccxt_module)
|
return exchange_name in ccxt_exchanges(ccxt_module)
|
||||||
|
|
||||||
|
|
||||||
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
def ccxt_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Return the list of all exchanges known to ccxt
|
Return the list of all exchanges known to ccxt
|
||||||
"""
|
"""
|
||||||
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
|
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
|
||||||
|
|
||||||
|
|
||||||
def available_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
def available_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list
|
Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list
|
||||||
"""
|
"""
|
||||||
@ -86,7 +87,7 @@ def timeframe_to_msecs(timeframe: str) -> int:
|
|||||||
return ccxt.Exchange.parse_timeframe(timeframe) * 1000
|
return ccxt.Exchange.parse_timeframe(timeframe) * 1000
|
||||||
|
|
||||||
|
|
||||||
def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
|
def timeframe_to_prev_date(timeframe: str, date: Optional[datetime] = None) -> datetime:
|
||||||
"""
|
"""
|
||||||
Use Timeframe and determine the candle start date for this date.
|
Use Timeframe and determine the candle start date for this date.
|
||||||
Does not round when given a candle start date.
|
Does not round when given a candle start date.
|
||||||
@ -102,7 +103,7 @@ def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
|
|||||||
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
|
def timeframe_to_next_date(timeframe: str, date: Optional[datetime] = None) -> datetime:
|
||||||
"""
|
"""
|
||||||
Use Timeframe and determine next candle.
|
Use Timeframe and determine next candle.
|
||||||
:param timeframe: timeframe in string format (e.g. "5m")
|
:param timeframe: timeframe in string format (e.g. "5m")
|
||||||
|
@ -5,7 +5,7 @@ import shutil
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from math import cos, sin
|
from math import cos, sin
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import numpy.typing as npt
|
import numpy.typing as npt
|
||||||
@ -112,7 +112,7 @@ class FreqaiDataKitchen:
|
|||||||
def set_paths(
|
def set_paths(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
trained_timestamp: int = None,
|
trained_timestamp: Optional[int] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Set the paths to the data for the present coin/botloop
|
Set the paths to the data for the present coin/botloop
|
||||||
|
@ -33,6 +33,7 @@ from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer
|
|||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
from freqtrade.util import FtPrecise
|
from freqtrade.util import FtPrecise
|
||||||
|
from freqtrade.util.binance_mig import migrate_binance_futures_names
|
||||||
from freqtrade.wallets import Wallets
|
from freqtrade.wallets import Wallets
|
||||||
|
|
||||||
|
|
||||||
@ -177,6 +178,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
Called on startup and after reloading the bot - triggers notifications and
|
Called on startup and after reloading the bot - triggers notifications and
|
||||||
performs startup tasks
|
performs startup tasks
|
||||||
"""
|
"""
|
||||||
|
migrate_binance_futures_names(self.config)
|
||||||
|
|
||||||
self.rpc.startup_messages(self.config, self.pairlists, self.protections)
|
self.rpc.startup_messages(self.config, self.pairlists, self.protections)
|
||||||
# Update older trades with precision and precision mode
|
# Update older trades with precision and precision mode
|
||||||
self.startup_backpopulate_precision()
|
self.startup_backpopulate_precision()
|
||||||
@ -1519,7 +1522,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
*,
|
*,
|
||||||
exit_tag: Optional[str] = None,
|
exit_tag: Optional[str] = None,
|
||||||
ordertype: Optional[str] = None,
|
ordertype: Optional[str] = None,
|
||||||
sub_trade_amt: float = None,
|
sub_trade_amt: Optional[float] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Executes a trade exit for the given trade and limit
|
Executes a trade exit for the given trade and limit
|
||||||
@ -1613,7 +1616,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False,
|
def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False,
|
||||||
sub_trade: bool = False, order: Order = None) -> None:
|
sub_trade: bool = False, order: Optional[Order] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Sends rpc notification when a sell occurred.
|
Sends rpc notification when a sell occurred.
|
||||||
"""
|
"""
|
||||||
@ -1726,7 +1729,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Common update trade state methods
|
# Common update trade state methods
|
||||||
#
|
#
|
||||||
|
|
||||||
def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None,
|
def update_trade_state(
|
||||||
|
self, trade: Trade, order_id: str, action_order: Optional[Dict[str, Any]] = None,
|
||||||
stoploss_order: bool = False, send_msg: bool = True) -> bool:
|
stoploss_order: bool = False, send_msg: bool = True) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks trades with open orders and updates the amount if necessary
|
Checks trades with open orders and updates the amount if necessary
|
||||||
|
@ -5,7 +5,7 @@ Read the documentation to know what cli arguments you need.
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from typing import Any, List
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
from freqtrade.util.gc_setup import gc_set_threshold
|
from freqtrade.util.gc_setup import gc_set_threshold
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ from freqtrade.loggers import setup_logging_pre
|
|||||||
logger = logging.getLogger('freqtrade')
|
logger = logging.getLogger('freqtrade')
|
||||||
|
|
||||||
|
|
||||||
def main(sysargv: List[str] = None) -> None:
|
def main(sysargv: Optional[List[str]] = None) -> None:
|
||||||
"""
|
"""
|
||||||
This function will initiate the bot and start the trading loop.
|
This function will initiate the bot and start the trading loop.
|
||||||
:return: None
|
:return: None
|
||||||
|
@ -6,7 +6,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Iterator, List, Mapping, Union
|
from typing import Any, Dict, Iterator, List, Mapping, Optional, Union
|
||||||
from typing.io import IO
|
from typing.io import IO
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@ -205,7 +205,7 @@ def safe_value_fallback2(dict1: dictMap, dict2: dictMap, key1: str, key2: str, d
|
|||||||
return default_value
|
return default_value
|
||||||
|
|
||||||
|
|
||||||
def plural(num: float, singular: str, plural: str = None) -> str:
|
def plural(num: float, singular: str, plural: Optional[str] = None) -> str:
|
||||||
return singular if (num == 1 or num == -1) else plural or singular + 's'
|
return singular if (num == 1 or num == -1) else plural or singular + 's'
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ from pandas import DataFrame
|
|||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.configuration import TimeRange, validate_config_consistency
|
from freqtrade.configuration import TimeRange, validate_config_consistency
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config, LongShort
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config, IntOrInf, LongShort
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe
|
from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe
|
||||||
from freqtrade.data.converter import trim_dataframe, trim_dataframes
|
from freqtrade.data.converter import trim_dataframe, trim_dataframes
|
||||||
@ -37,6 +37,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager
|
|||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
|
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
||||||
from freqtrade.wallets import Wallets
|
from freqtrade.wallets import Wallets
|
||||||
|
|
||||||
|
|
||||||
@ -157,6 +158,7 @@ class Backtesting:
|
|||||||
self._can_short = self.trading_mode != TradingMode.SPOT
|
self._can_short = self.trading_mode != TradingMode.SPOT
|
||||||
self._position_stacking: bool = self.config.get('position_stacking', False)
|
self._position_stacking: bool = self.config.get('position_stacking', False)
|
||||||
self.enable_protections: bool = self.config.get('enable_protections', False)
|
self.enable_protections: bool = self.config.get('enable_protections', False)
|
||||||
|
migrate_binance_futures_data(config)
|
||||||
|
|
||||||
self.init_backtest()
|
self.init_backtest()
|
||||||
|
|
||||||
@ -573,26 +575,6 @@ class Backtesting:
|
|||||||
""" Rate is within candle, therefore filled"""
|
""" Rate is within candle, therefore filled"""
|
||||||
return row[LOW_IDX] <= rate <= row[HIGH_IDX]
|
return row[LOW_IDX] <= rate <= row[HIGH_IDX]
|
||||||
|
|
||||||
def _get_exit_trade_entry_for_candle(self, trade: LocalTrade,
|
|
||||||
row: Tuple) -> Optional[LocalTrade]:
|
|
||||||
|
|
||||||
# Check if we need to adjust our current positions
|
|
||||||
if self.strategy.position_adjustment_enable:
|
|
||||||
trade = self._get_adjust_trade_entry_for_candle(trade, row)
|
|
||||||
|
|
||||||
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
|
|
||||||
exit_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
|
|
||||||
exits = self.strategy.should_exit(
|
|
||||||
trade, row[OPEN_IDX], row[DATE_IDX].to_pydatetime(), # type: ignore
|
|
||||||
enter=enter, exit_=exit_sig,
|
|
||||||
low=row[LOW_IDX], high=row[HIGH_IDX]
|
|
||||||
)
|
|
||||||
for exit_ in exits:
|
|
||||||
t = self._get_exit_for_signal(trade, row, exit_)
|
|
||||||
if t:
|
|
||||||
return t
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_exit_for_signal(
|
def _get_exit_for_signal(
|
||||||
self, trade: LocalTrade, row: Tuple, exit_: ExitCheckTuple,
|
self, trade: LocalTrade, row: Tuple, exit_: ExitCheckTuple,
|
||||||
amount: Optional[float] = None) -> Optional[LocalTrade]:
|
amount: Optional[float] = None) -> Optional[LocalTrade]:
|
||||||
@ -662,7 +644,7 @@ class Backtesting:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _exit_trade(self, trade: LocalTrade, sell_row: Tuple,
|
def _exit_trade(self, trade: LocalTrade, sell_row: Tuple,
|
||||||
close_rate: float, amount: float = None) -> Optional[LocalTrade]:
|
close_rate: float, amount: Optional[float] = None) -> Optional[LocalTrade]:
|
||||||
self.order_id_counter += 1
|
self.order_id_counter += 1
|
||||||
exit_candle_time = sell_row[DATE_IDX].to_pydatetime()
|
exit_candle_time = sell_row[DATE_IDX].to_pydatetime()
|
||||||
order_type = self.strategy.order_types['exit']
|
order_type = self.strategy.order_types['exit']
|
||||||
@ -692,11 +674,10 @@ class Backtesting:
|
|||||||
trade.orders.append(order)
|
trade.orders.append(order)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
def _get_exit_trade_entry(
|
def _check_trade_exit(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]:
|
||||||
self, trade: LocalTrade, row: Tuple, is_first: bool) -> Optional[LocalTrade]:
|
|
||||||
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||||
|
|
||||||
if is_first and self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
trade.funding_fees = self.exchange.calculate_funding_fees(
|
trade.funding_fees = self.exchange.calculate_funding_fees(
|
||||||
self.futures_data[trade.pair],
|
self.futures_data[trade.pair],
|
||||||
amount=trade.amount,
|
amount=trade.amount,
|
||||||
@ -705,7 +686,22 @@ class Backtesting:
|
|||||||
close_date=exit_candle_time,
|
close_date=exit_candle_time,
|
||||||
)
|
)
|
||||||
|
|
||||||
return self._get_exit_trade_entry_for_candle(trade, row)
|
# Check if we need to adjust our current positions
|
||||||
|
if self.strategy.position_adjustment_enable:
|
||||||
|
trade = self._get_adjust_trade_entry_for_candle(trade, row)
|
||||||
|
|
||||||
|
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
|
||||||
|
exit_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
|
||||||
|
exits = self.strategy.should_exit(
|
||||||
|
trade, row[OPEN_IDX], row[DATE_IDX].to_pydatetime(), # type: ignore
|
||||||
|
enter=enter, exit_=exit_sig,
|
||||||
|
low=row[LOW_IDX], high=row[HIGH_IDX]
|
||||||
|
)
|
||||||
|
for exit_ in exits:
|
||||||
|
t = self._get_exit_for_signal(trade, row, exit_)
|
||||||
|
if t:
|
||||||
|
return t
|
||||||
|
return None
|
||||||
|
|
||||||
def get_valid_price_and_stake(
|
def get_valid_price_and_stake(
|
||||||
self, pair: str, row: Tuple, propose_rate: float, stake_amount: float,
|
self, pair: str, row: Tuple, propose_rate: float, stake_amount: float,
|
||||||
@ -920,8 +916,9 @@ class Backtesting:
|
|||||||
trade.close(exit_row[OPEN_IDX], show_msg=False)
|
trade.close(exit_row[OPEN_IDX], show_msg=False)
|
||||||
LocalTrade.close_bt_trade(trade)
|
LocalTrade.close_bt_trade(trade)
|
||||||
|
|
||||||
def trade_slot_available(self, max_open_trades: int, open_trade_count: int) -> bool:
|
def trade_slot_available(self, open_trade_count: int) -> bool:
|
||||||
# Always allow trades when max_open_trades is enabled.
|
# Always allow trades when max_open_trades is enabled.
|
||||||
|
max_open_trades: IntOrInf = self.config['max_open_trades']
|
||||||
if max_open_trades <= 0 or open_trade_count < max_open_trades:
|
if max_open_trades <= 0 or open_trade_count < max_open_trades:
|
||||||
return True
|
return True
|
||||||
# Rejected trade
|
# Rejected trade
|
||||||
@ -1051,7 +1048,7 @@ class Backtesting:
|
|||||||
|
|
||||||
def backtest_loop(
|
def backtest_loop(
|
||||||
self, row: Tuple, pair: str, current_time: datetime, end_date: datetime,
|
self, row: Tuple, pair: str, current_time: datetime, end_date: datetime,
|
||||||
max_open_trades: int, open_trade_count_start: int, trade_dir: Optional[LongShort],
|
open_trade_count_start: int, trade_dir: Optional[LongShort],
|
||||||
is_first: bool = True) -> int:
|
is_first: bool = True) -> int:
|
||||||
"""
|
"""
|
||||||
NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
|
NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
|
||||||
@ -1074,7 +1071,7 @@ class Backtesting:
|
|||||||
if (
|
if (
|
||||||
(self._position_stacking or len(LocalTrade.bt_trades_open_pp[pair]) == 0)
|
(self._position_stacking or len(LocalTrade.bt_trades_open_pp[pair]) == 0)
|
||||||
and is_first
|
and is_first
|
||||||
and self.trade_slot_available(max_open_trades, open_trade_count_start)
|
and self.trade_slot_available(open_trade_count_start)
|
||||||
and current_time != end_date
|
and current_time != end_date
|
||||||
and trade_dir is not None
|
and trade_dir is not None
|
||||||
and not PairLocks.is_pair_locked(pair, row[DATE_IDX], trade_dir)
|
and not PairLocks.is_pair_locked(pair, row[DATE_IDX], trade_dir)
|
||||||
@ -1099,7 +1096,7 @@ class Backtesting:
|
|||||||
|
|
||||||
# 4. Create exit orders (if any)
|
# 4. Create exit orders (if any)
|
||||||
if not trade.open_order_id:
|
if not trade.open_order_id:
|
||||||
self._get_exit_trade_entry(trade, row, is_first) # Place exit order if necessary
|
self._check_trade_exit(trade, row) # Place exit order if necessary
|
||||||
|
|
||||||
# 5. Process exit orders.
|
# 5. Process exit orders.
|
||||||
order = trade.select_order(trade.exit_side, is_open=True)
|
order = trade.select_order(trade.exit_side, is_open=True)
|
||||||
@ -1121,8 +1118,7 @@ class Backtesting:
|
|||||||
return open_trade_count_start
|
return open_trade_count_start
|
||||||
|
|
||||||
def backtest(self, processed: Dict,
|
def backtest(self, processed: Dict,
|
||||||
start_date: datetime, end_date: datetime,
|
start_date: datetime, end_date: datetime) -> Dict[str, Any]:
|
||||||
max_open_trades: int = 0) -> Dict[str, Any]:
|
|
||||||
"""
|
"""
|
||||||
Implement backtesting functionality
|
Implement backtesting functionality
|
||||||
|
|
||||||
@ -1134,7 +1130,6 @@ class Backtesting:
|
|||||||
optimize memory usage!
|
optimize memory usage!
|
||||||
:param start_date: backtesting timerange start datetime
|
:param start_date: backtesting timerange start datetime
|
||||||
:param end_date: backtesting timerange end datetime
|
:param end_date: backtesting timerange end datetime
|
||||||
:param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited
|
|
||||||
:return: DataFrame with trades (results of backtesting)
|
:return: DataFrame with trades (results of backtesting)
|
||||||
"""
|
"""
|
||||||
self.prepare_backtest(self.enable_protections)
|
self.prepare_backtest(self.enable_protections)
|
||||||
@ -1183,7 +1178,7 @@ class Backtesting:
|
|||||||
if len(detail_data) == 0:
|
if len(detail_data) == 0:
|
||||||
# Fall back to "regular" data if no detail data was found for this candle
|
# Fall back to "regular" data if no detail data was found for this candle
|
||||||
open_trade_count_start = self.backtest_loop(
|
open_trade_count_start = self.backtest_loop(
|
||||||
row, pair, current_time, end_date, max_open_trades,
|
row, pair, current_time, end_date,
|
||||||
open_trade_count_start, trade_dir)
|
open_trade_count_start, trade_dir)
|
||||||
continue
|
continue
|
||||||
detail_data.loc[:, 'enter_long'] = row[LONG_IDX]
|
detail_data.loc[:, 'enter_long'] = row[LONG_IDX]
|
||||||
@ -1196,13 +1191,13 @@ class Backtesting:
|
|||||||
current_time_det = current_time
|
current_time_det = current_time
|
||||||
for det_row in detail_data[HEADERS].values.tolist():
|
for det_row in detail_data[HEADERS].values.tolist():
|
||||||
open_trade_count_start = self.backtest_loop(
|
open_trade_count_start = self.backtest_loop(
|
||||||
det_row, pair, current_time_det, end_date, max_open_trades,
|
det_row, pair, current_time_det, end_date,
|
||||||
open_trade_count_start, trade_dir, is_first)
|
open_trade_count_start, trade_dir, is_first)
|
||||||
current_time_det += timedelta(minutes=self.timeframe_detail_min)
|
current_time_det += timedelta(minutes=self.timeframe_detail_min)
|
||||||
is_first = False
|
is_first = False
|
||||||
else:
|
else:
|
||||||
open_trade_count_start = self.backtest_loop(
|
open_trade_count_start = self.backtest_loop(
|
||||||
row, pair, current_time, end_date, max_open_trades,
|
row, pair, current_time, end_date,
|
||||||
open_trade_count_start, trade_dir)
|
open_trade_count_start, trade_dir)
|
||||||
|
|
||||||
# Move time one configured time_interval ahead.
|
# Move time one configured time_interval ahead.
|
||||||
@ -1235,13 +1230,11 @@ class Backtesting:
|
|||||||
self._set_strategy(strat)
|
self._set_strategy(strat)
|
||||||
|
|
||||||
# Use max_open_trades in backtesting, except --disable-max-market-positions is set
|
# Use max_open_trades in backtesting, except --disable-max-market-positions is set
|
||||||
if self.config.get('use_max_market_positions', True):
|
if not self.config.get('use_max_market_positions', True):
|
||||||
# Must come from strategy config, as the strategy may modify this setting.
|
|
||||||
max_open_trades = self.strategy.config['max_open_trades']
|
|
||||||
else:
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'Ignoring max_open_trades (--disable-max-market-positions was used) ...')
|
'Ignoring max_open_trades (--disable-max-market-positions was used) ...')
|
||||||
max_open_trades = 0
|
self.strategy.max_open_trades = float('inf')
|
||||||
|
self.config.update({'max_open_trades': self.strategy.max_open_trades})
|
||||||
|
|
||||||
# need to reprocess data every time to populate signals
|
# need to reprocess data every time to populate signals
|
||||||
preprocessed = self.strategy.advise_all_indicators(data)
|
preprocessed = self.strategy.advise_all_indicators(data)
|
||||||
@ -1264,7 +1257,6 @@ class Backtesting:
|
|||||||
processed=preprocessed,
|
processed=preprocessed,
|
||||||
start_date=min_date,
|
start_date=min_date,
|
||||||
end_date=max_date,
|
end_date=max_date,
|
||||||
max_open_trades=max_open_trades,
|
|
||||||
)
|
)
|
||||||
backtest_end_time = datetime.now(timezone.utc)
|
backtest_end_time = datetime.now(timezone.utc)
|
||||||
results.update({
|
results.update({
|
||||||
|
@ -74,6 +74,7 @@ class Hyperopt:
|
|||||||
self.roi_space: List[Dimension] = []
|
self.roi_space: List[Dimension] = []
|
||||||
self.stoploss_space: List[Dimension] = []
|
self.stoploss_space: List[Dimension] = []
|
||||||
self.trailing_space: List[Dimension] = []
|
self.trailing_space: List[Dimension] = []
|
||||||
|
self.max_open_trades_space: List[Dimension] = []
|
||||||
self.dimensions: List[Dimension] = []
|
self.dimensions: List[Dimension] = []
|
||||||
|
|
||||||
self.config = config
|
self.config = config
|
||||||
@ -117,11 +118,10 @@ class Hyperopt:
|
|||||||
self.current_best_epoch: Optional[Dict[str, Any]] = None
|
self.current_best_epoch: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
|
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
|
||||||
if self.config.get('use_max_market_positions', True):
|
if not self.config.get('use_max_market_positions', True):
|
||||||
self.max_open_trades = self.config['max_open_trades']
|
|
||||||
else:
|
|
||||||
logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
|
logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
|
||||||
self.max_open_trades = 0
|
self.backtesting.strategy.max_open_trades = float('inf')
|
||||||
|
config.update({'max_open_trades': self.backtesting.strategy.max_open_trades})
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, 'sell'):
|
if HyperoptTools.has_space(self.config, 'sell'):
|
||||||
# Make sure use_exit_signal is enabled
|
# Make sure use_exit_signal is enabled
|
||||||
@ -209,6 +209,10 @@ class Hyperopt:
|
|||||||
result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space}
|
result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space}
|
||||||
if HyperoptTools.has_space(self.config, 'trailing'):
|
if HyperoptTools.has_space(self.config, 'trailing'):
|
||||||
result['trailing'] = self.custom_hyperopt.generate_trailing_params(params)
|
result['trailing'] = self.custom_hyperopt.generate_trailing_params(params)
|
||||||
|
if HyperoptTools.has_space(self.config, 'trades'):
|
||||||
|
result['max_open_trades'] = {
|
||||||
|
'max_open_trades': self.backtesting.strategy.max_open_trades
|
||||||
|
if self.backtesting.strategy.max_open_trades != float('inf') else -1}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -229,6 +233,8 @@ class Hyperopt:
|
|||||||
'trailing_stop_positive_offset': strategy.trailing_stop_positive_offset,
|
'trailing_stop_positive_offset': strategy.trailing_stop_positive_offset,
|
||||||
'trailing_only_offset_is_reached': strategy.trailing_only_offset_is_reached,
|
'trailing_only_offset_is_reached': strategy.trailing_only_offset_is_reached,
|
||||||
}
|
}
|
||||||
|
if not HyperoptTools.has_space(self.config, 'trades'):
|
||||||
|
result['max_open_trades'] = {'max_open_trades': strategy.max_open_trades}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def print_results(self, results) -> None:
|
def print_results(self, results) -> None:
|
||||||
@ -280,8 +286,13 @@ class Hyperopt:
|
|||||||
logger.debug("Hyperopt has 'trailing' space")
|
logger.debug("Hyperopt has 'trailing' space")
|
||||||
self.trailing_space = self.custom_hyperopt.trailing_space()
|
self.trailing_space = self.custom_hyperopt.trailing_space()
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, 'trades'):
|
||||||
|
logger.debug("Hyperopt has 'trades' space")
|
||||||
|
self.max_open_trades_space = self.custom_hyperopt.max_open_trades_space()
|
||||||
|
|
||||||
self.dimensions = (self.buy_space + self.sell_space + self.protection_space
|
self.dimensions = (self.buy_space + self.sell_space + self.protection_space
|
||||||
+ self.roi_space + self.stoploss_space + self.trailing_space)
|
+ self.roi_space + self.stoploss_space + self.trailing_space
|
||||||
|
+ self.max_open_trades_space)
|
||||||
|
|
||||||
def assign_params(self, params_dict: Dict, category: str) -> None:
|
def assign_params(self, params_dict: Dict, category: str) -> None:
|
||||||
"""
|
"""
|
||||||
@ -328,6 +339,20 @@ class Hyperopt:
|
|||||||
self.backtesting.strategy.trailing_only_offset_is_reached = \
|
self.backtesting.strategy.trailing_only_offset_is_reached = \
|
||||||
d['trailing_only_offset_is_reached']
|
d['trailing_only_offset_is_reached']
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, 'trades'):
|
||||||
|
if self.config["stake_amount"] == "unlimited" and \
|
||||||
|
(params_dict['max_open_trades'] == -1 or params_dict['max_open_trades'] == 0):
|
||||||
|
# Ignore unlimited max open trades if stake amount is unlimited
|
||||||
|
params_dict.update({'max_open_trades': self.config['max_open_trades']})
|
||||||
|
|
||||||
|
updated_max_open_trades = int(params_dict['max_open_trades']) \
|
||||||
|
if (params_dict['max_open_trades'] != -1
|
||||||
|
and params_dict['max_open_trades'] != 0) else float('inf')
|
||||||
|
|
||||||
|
self.config.update({'max_open_trades': updated_max_open_trades})
|
||||||
|
|
||||||
|
self.backtesting.strategy.max_open_trades = updated_max_open_trades
|
||||||
|
|
||||||
with self.data_pickle_file.open('rb') as f:
|
with self.data_pickle_file.open('rb') as f:
|
||||||
processed = load(f, mmap_mode='r')
|
processed = load(f, mmap_mode='r')
|
||||||
if self.analyze_per_epoch:
|
if self.analyze_per_epoch:
|
||||||
@ -337,8 +362,7 @@ class Hyperopt:
|
|||||||
bt_results = self.backtesting.backtest(
|
bt_results = self.backtesting.backtest(
|
||||||
processed=processed,
|
processed=processed,
|
||||||
start_date=self.min_date,
|
start_date=self.min_date,
|
||||||
end_date=self.max_date,
|
end_date=self.max_date
|
||||||
max_open_trades=self.max_open_trades,
|
|
||||||
)
|
)
|
||||||
backtest_end_time = datetime.now(timezone.utc)
|
backtest_end_time = datetime.now(timezone.utc)
|
||||||
bt_results.update({
|
bt_results.update({
|
||||||
|
@ -91,5 +91,8 @@ class HyperOptAuto(IHyperOpt):
|
|||||||
def trailing_space(self) -> List['Dimension']:
|
def trailing_space(self) -> List['Dimension']:
|
||||||
return self._get_func('trailing_space')()
|
return self._get_func('trailing_space')()
|
||||||
|
|
||||||
|
def max_open_trades_space(self) -> List['Dimension']:
|
||||||
|
return self._get_func('max_open_trades_space')()
|
||||||
|
|
||||||
def generate_estimator(self, dimensions: List['Dimension'], **kwargs) -> EstimatorType:
|
def generate_estimator(self, dimensions: List['Dimension'], **kwargs) -> EstimatorType:
|
||||||
return self._get_func('generate_estimator')(dimensions=dimensions, **kwargs)
|
return self._get_func('generate_estimator')(dimensions=dimensions, **kwargs)
|
||||||
|
@ -191,6 +191,16 @@ class IHyperOpt(ABC):
|
|||||||
Categorical([True, False], name='trailing_only_offset_is_reached'),
|
Categorical([True, False], name='trailing_only_offset_is_reached'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def max_open_trades_space(self) -> List[Dimension]:
|
||||||
|
"""
|
||||||
|
Create a max open trades space.
|
||||||
|
|
||||||
|
You may override it in your custom Hyperopt class.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
Integer(-1, 10, name='max_open_trades'),
|
||||||
|
]
|
||||||
|
|
||||||
# This is needed for proper unpickling the class attribute timeframe
|
# This is needed for proper unpickling the class attribute timeframe
|
||||||
# which is set to the actual value by the resolver.
|
# which is set to the actual value by the resolver.
|
||||||
# Why do I still need such shamanic mantras in modern python?
|
# Why do I still need such shamanic mantras in modern python?
|
||||||
|
@ -96,7 +96,7 @@ class HyperoptTools():
|
|||||||
Tell if the space value is contained in the configuration
|
Tell if the space value is contained in the configuration
|
||||||
"""
|
"""
|
||||||
# 'trailing' and 'protection spaces are not included in the 'default' set of spaces
|
# 'trailing' and 'protection spaces are not included in the 'default' set of spaces
|
||||||
if space in ('trailing', 'protection'):
|
if space in ('trailing', 'protection', 'trades'):
|
||||||
return any(s in config['spaces'] for s in [space, 'all'])
|
return any(s in config['spaces'] for s in [space, 'all'])
|
||||||
else:
|
else:
|
||||||
return any(s in config['spaces'] for s in [space, 'all', 'default'])
|
return any(s in config['spaces'] for s in [space, 'all', 'default'])
|
||||||
@ -170,7 +170,7 @@ class HyperoptTools():
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def show_epoch_details(results, total_epochs: int, print_json: bool,
|
def show_epoch_details(results, total_epochs: int, print_json: bool,
|
||||||
no_header: bool = False, header_str: str = None) -> None:
|
no_header: bool = False, header_str: Optional[str] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Display details of the hyperopt result
|
Display details of the hyperopt result
|
||||||
"""
|
"""
|
||||||
@ -187,7 +187,8 @@ class HyperoptTools():
|
|||||||
|
|
||||||
if print_json:
|
if print_json:
|
||||||
result_dict: Dict = {}
|
result_dict: Dict = {}
|
||||||
for s in ['buy', 'sell', 'protection', 'roi', 'stoploss', 'trailing']:
|
for s in ['buy', 'sell', 'protection',
|
||||||
|
'roi', 'stoploss', 'trailing', 'max_open_trades']:
|
||||||
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
|
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
|
||||||
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
|
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
|
||||||
|
|
||||||
@ -201,6 +202,8 @@ class HyperoptTools():
|
|||||||
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
|
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
|
||||||
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
|
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
|
||||||
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
|
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
|
||||||
|
HyperoptTools._params_pretty_print(
|
||||||
|
params, 'max_open_trades', "Max Open Trades:", non_optimized)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
|
def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
|
||||||
@ -239,7 +242,9 @@ class HyperoptTools():
|
|||||||
if space == "stoploss":
|
if space == "stoploss":
|
||||||
stoploss = safe_value_fallback2(space_params, no_params, space, space)
|
stoploss = safe_value_fallback2(space_params, no_params, space, space)
|
||||||
result += (f"stoploss = {stoploss}{appendix}")
|
result += (f"stoploss = {stoploss}{appendix}")
|
||||||
|
elif space == "max_open_trades":
|
||||||
|
max_open_trades = safe_value_fallback2(space_params, no_params, space, space)
|
||||||
|
result += (f"max_open_trades = {max_open_trades}{appendix}")
|
||||||
elif space == "roi":
|
elif space == "roi":
|
||||||
result = result[:-1] + f'{appendix}\n'
|
result = result[:-1] + f'{appendix}\n'
|
||||||
minimal_roi_result = rapidjson.dumps({
|
minimal_roi_result = rapidjson.dumps({
|
||||||
@ -259,7 +264,7 @@ class HyperoptTools():
|
|||||||
print(result)
|
print(result)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _space_params(params, space: str, r: int = None) -> Dict:
|
def _space_params(params, space: str, r: Optional[int] = None) -> Dict:
|
||||||
d = params.get(space)
|
d = params.get(space)
|
||||||
if d:
|
if d:
|
||||||
# Round floats to `r` digits after the decimal point if requested
|
# Round floats to `r` digits after the decimal point if requested
|
||||||
|
@ -8,7 +8,7 @@ from pandas import DataFrame, to_datetime
|
|||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
from freqtrade.constants import (DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT,
|
from freqtrade.constants import (DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT,
|
||||||
Config)
|
Config, IntOrInf)
|
||||||
from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
|
from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
|
||||||
calculate_expectancy, calculate_market_change,
|
calculate_expectancy, calculate_market_change,
|
||||||
calculate_max_drawdown, calculate_sharpe, calculate_sortino)
|
calculate_max_drawdown, calculate_sharpe, calculate_sortino)
|
||||||
@ -191,7 +191,7 @@ def generate_tag_metrics(tag_type: str,
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def generate_exit_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
|
def generate_exit_reason_stats(max_open_trades: IntOrInf, results: DataFrame) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Generate small table outlining Backtest results
|
Generate small table outlining Backtest results
|
||||||
:param max_open_trades: Max_open_trades parameter
|
:param max_open_trades: Max_open_trades parameter
|
||||||
|
@ -30,8 +30,8 @@ class PairLocks():
|
|||||||
PairLocks.locks = []
|
PairLocks.locks = []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def lock_pair(pair: str, until: datetime, reason: str = None, *,
|
def lock_pair(pair: str, until: datetime, reason: Optional[str] = None, *,
|
||||||
now: datetime = None, side: str = '*') -> PairLock:
|
now: Optional[datetime] = None, side: str = '*') -> PairLock:
|
||||||
"""
|
"""
|
||||||
Create PairLock from now to "until".
|
Create PairLock from now to "until".
|
||||||
Uses database by default, unless PairLocks.use_db is set to False,
|
Uses database by default, unless PairLocks.use_db is set to False,
|
||||||
|
@ -146,7 +146,7 @@ class Order(_DECL_BASE):
|
|||||||
# Assign funding fee up to this point
|
# Assign funding fee up to this point
|
||||||
# (represents the funding fee since the last order)
|
# (represents the funding fee since the last order)
|
||||||
self.funding_fee = self.trade.funding_fees
|
self.funding_fee = self.trade.funding_fees
|
||||||
if (order.get('filled', 0.0) or 0.0) > 0:
|
if (order.get('filled', 0.0) or 0.0) > 0 and not self.order_filled_date:
|
||||||
self.order_filled_date = datetime.now(timezone.utc)
|
self.order_filled_date = datetime.now(timezone.utc)
|
||||||
self.order_update_date = datetime.now(timezone.utc)
|
self.order_update_date = datetime.now(timezone.utc)
|
||||||
|
|
||||||
@ -799,7 +799,7 @@ class LocalTrade():
|
|||||||
else:
|
else:
|
||||||
return close_trade - fees
|
return close_trade - fees
|
||||||
|
|
||||||
def calc_close_trade_value(self, rate: float, amount: float = None) -> float:
|
def calc_close_trade_value(self, rate: float, amount: Optional[float] = None) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate the Trade's close value including fees
|
Calculate the Trade's close value including fees
|
||||||
:param rate: rate to compare with.
|
:param rate: rate to compare with.
|
||||||
@ -837,7 +837,8 @@ class LocalTrade():
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"{self.trading_mode.value} trading is not yet available using freqtrade")
|
f"{self.trading_mode.value} trading is not yet available using freqtrade")
|
||||||
|
|
||||||
def calc_profit(self, rate: float, amount: float = None, open_rate: float = None) -> float:
|
def calc_profit(self, rate: float, amount: Optional[float] = None,
|
||||||
|
open_rate: Optional[float] = None) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate the absolute profit in stake currency between Close and Open trade
|
Calculate the absolute profit in stake currency between Close and Open trade
|
||||||
:param rate: close rate to compare with.
|
:param rate: close rate to compare with.
|
||||||
@ -858,7 +859,8 @@ class LocalTrade():
|
|||||||
return float(f"{profit:.8f}")
|
return float(f"{profit:.8f}")
|
||||||
|
|
||||||
def calc_profit_ratio(
|
def calc_profit_ratio(
|
||||||
self, rate: float, amount: float = None, open_rate: float = None) -> float:
|
self, rate: float, amount: Optional[float] = None,
|
||||||
|
open_rate: Optional[float] = None) -> float:
|
||||||
"""
|
"""
|
||||||
Calculates the profit as ratio (including fee).
|
Calculates the profit as ratio (including fee).
|
||||||
:param rate: rate to compare with.
|
:param rate: rate to compare with.
|
||||||
@ -1059,8 +1061,9 @@ class LocalTrade():
|
|||||||
return self.exit_reason
|
return self.exit_reason
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
|
def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None,
|
||||||
open_date: datetime = None, close_date: datetime = None,
|
open_date: Optional[datetime] = None,
|
||||||
|
close_date: Optional[datetime] = None,
|
||||||
) -> List['LocalTrade']:
|
) -> List['LocalTrade']:
|
||||||
"""
|
"""
|
||||||
Helper function to query Trades.
|
Helper function to query Trades.
|
||||||
@ -1257,8 +1260,9 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||||||
Trade.query.session.rollback()
|
Trade.query.session.rollback()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
|
def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None,
|
||||||
open_date: datetime = None, close_date: datetime = None,
|
open_date: Optional[datetime] = None,
|
||||||
|
close_date: Optional[datetime] = None,
|
||||||
) -> List['LocalTrade']:
|
) -> List['LocalTrade']:
|
||||||
"""
|
"""
|
||||||
Helper function to query Trades.j
|
Helper function to query Trades.j
|
||||||
|
@ -436,9 +436,9 @@ def create_scatter(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *,
|
def generate_candlestick_graph(
|
||||||
indicators1: List[str] = [],
|
pair: str, data: pd.DataFrame, trades: Optional[pd.DataFrame] = None, *,
|
||||||
indicators2: List[str] = [],
|
indicators1: List[str] = [], indicators2: List[str] = [],
|
||||||
plot_config: Dict[str, Dict] = {},
|
plot_config: Dict[str, Dict] = {},
|
||||||
) -> go.Figure:
|
) -> go.Figure:
|
||||||
"""
|
"""
|
||||||
|
@ -23,7 +23,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class PairListManager(LoggingMixin):
|
class PairListManager(LoggingMixin):
|
||||||
|
|
||||||
def __init__(self, exchange, config: Config, dataprovider: DataProvider = None) -> None:
|
def __init__(
|
||||||
|
self, exchange, config: Config, dataprovider: Optional[DataProvider] = None) -> None:
|
||||||
self._exchange = exchange
|
self._exchange = exchange
|
||||||
self._config = config
|
self._config = config
|
||||||
self._whitelist = self._config['exchange'].get('pair_whitelist')
|
self._whitelist = self._config['exchange'].get('pair_whitelist')
|
||||||
@ -153,7 +154,8 @@ class PairListManager(LoggingMixin):
|
|||||||
return []
|
return []
|
||||||
return whitelist
|
return whitelist
|
||||||
|
|
||||||
def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes:
|
def create_pair_list(
|
||||||
|
self, pairs: List[str], timeframe: Optional[str] = None) -> ListPairsWithTimeframes:
|
||||||
"""
|
"""
|
||||||
Create list of pair tuples with (pair, timeframe)
|
Create list of pair tuples with (pair, timeframe)
|
||||||
"""
|
"""
|
||||||
|
@ -89,7 +89,8 @@ class IResolver:
|
|||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
try:
|
try:
|
||||||
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
|
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
|
||||||
except (ModuleNotFoundError, SyntaxError, ImportError, NameError) as err:
|
except (AttributeError, ModuleNotFoundError, SyntaxError,
|
||||||
|
ImportError, NameError) as err:
|
||||||
# Catch errors in case a specific module is not installed
|
# Catch errors in case a specific module is not installed
|
||||||
logger.warning(f"Could not import {module_path} due to '{err}'")
|
logger.warning(f"Could not import {module_path} due to '{err}'")
|
||||||
if enum_failed:
|
if enum_failed:
|
||||||
|
@ -33,7 +33,7 @@ class StrategyResolver(IResolver):
|
|||||||
extra_path = "strategy_path"
|
extra_path = "strategy_path"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_strategy(config: Config = None) -> IStrategy:
|
def load_strategy(config: Optional[Config] = None) -> IStrategy:
|
||||||
"""
|
"""
|
||||||
Load the custom class from config parameter
|
Load the custom class from config parameter
|
||||||
:param config: configuration dictionary or None
|
:param config: configuration dictionary or None
|
||||||
@ -76,6 +76,7 @@ class StrategyResolver(IResolver):
|
|||||||
("ignore_buying_expired_candle_after", 0),
|
("ignore_buying_expired_candle_after", 0),
|
||||||
("position_adjustment_enable", False),
|
("position_adjustment_enable", False),
|
||||||
("max_entry_position_adjustment", -1),
|
("max_entry_position_adjustment", -1),
|
||||||
|
("max_open_trades", -1)
|
||||||
]
|
]
|
||||||
for attribute, default in attributes:
|
for attribute, default in attributes:
|
||||||
StrategyResolver._override_attribute_helper(strategy, config,
|
StrategyResolver._override_attribute_helper(strategy, config,
|
||||||
@ -110,6 +111,10 @@ class StrategyResolver(IResolver):
|
|||||||
val = getattr(strategy, attribute)
|
val = getattr(strategy, attribute)
|
||||||
# None's cannot exist in the config, so do not copy them
|
# None's cannot exist in the config, so do not copy them
|
||||||
if val is not None:
|
if val is not None:
|
||||||
|
# max_open_trades set to -1 in the strategy will be copied as infinity in the config
|
||||||
|
if attribute == 'max_open_trades' and val == -1:
|
||||||
|
config[attribute] = float('inf')
|
||||||
|
else:
|
||||||
config[attribute] = val
|
config[attribute] = val
|
||||||
# Explicitly check for None here as other "falsy" values are possible
|
# Explicitly check for None here as other "falsy" values are possible
|
||||||
elif default is not None:
|
elif default is not None:
|
||||||
@ -128,6 +133,8 @@ class StrategyResolver(IResolver):
|
|||||||
key=lambda t: t[0]))
|
key=lambda t: t[0]))
|
||||||
if hasattr(strategy, 'stoploss'):
|
if hasattr(strategy, 'stoploss'):
|
||||||
strategy.stoploss = float(strategy.stoploss)
|
strategy.stoploss = float(strategy.stoploss)
|
||||||
|
if hasattr(strategy, 'max_open_trades') and strategy.max_open_trades < 0:
|
||||||
|
strategy.max_open_trades = float('inf')
|
||||||
return strategy
|
return strategy
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional, Union
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, IntOrInf
|
||||||
from freqtrade.enums import OrderTypeValues, SignalDirection, TradingMode
|
from freqtrade.enums import OrderTypeValues, SignalDirection, TradingMode
|
||||||
|
|
||||||
|
|
||||||
@ -165,7 +165,7 @@ class ShowConfig(BaseModel):
|
|||||||
stake_amount: str
|
stake_amount: str
|
||||||
available_capital: Optional[float]
|
available_capital: Optional[float]
|
||||||
stake_currency_decimals: int
|
stake_currency_decimals: int
|
||||||
max_open_trades: int
|
max_open_trades: IntOrInf
|
||||||
minimal_roi: Dict[str, Any]
|
minimal_roi: Dict[str, Any]
|
||||||
stoploss: Optional[float]
|
stoploss: Optional[float]
|
||||||
trailing_stop: Optional[bool]
|
trailing_stop: Optional[bool]
|
||||||
@ -422,7 +422,7 @@ class BacktestRequest(BaseModel):
|
|||||||
timeframe: Optional[str]
|
timeframe: Optional[str]
|
||||||
timeframe_detail: Optional[str]
|
timeframe_detail: Optional[str]
|
||||||
timerange: Optional[str]
|
timerange: Optional[str]
|
||||||
max_open_trades: Optional[int]
|
max_open_trades: Optional[IntOrInf]
|
||||||
stake_amount: Optional[str]
|
stake_amount: Optional[str]
|
||||||
enable_protections: bool
|
enable_protections: bool
|
||||||
dry_run_wallet: Optional[float]
|
dry_run_wallet: Optional[float]
|
||||||
|
@ -40,7 +40,8 @@ logger = logging.getLogger(__name__)
|
|||||||
# 2.20: Add websocket endpoints
|
# 2.20: Add websocket endpoints
|
||||||
# 2.21: Add new_candle messagetype
|
# 2.21: Add new_candle messagetype
|
||||||
# 2.22: Add FreqAI to backtesting
|
# 2.22: Add FreqAI to backtesting
|
||||||
API_VERSION = 2.22
|
# 2.23: Allow plot config request in webserver mode
|
||||||
|
API_VERSION = 2.23
|
||||||
|
|
||||||
# Public API, requires no auth.
|
# Public API, requires no auth.
|
||||||
router_public = APIRouter()
|
router_public = APIRouter()
|
||||||
@ -248,8 +249,18 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str,
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/plot_config', response_model=PlotConfig, tags=['candle data'])
|
@router.get('/plot_config', response_model=PlotConfig, tags=['candle data'])
|
||||||
def plot_config(rpc: RPC = Depends(get_rpc)):
|
def plot_config(strategy: Optional[str] = None, config=Depends(get_config),
|
||||||
|
rpc: Optional[RPC] = Depends(get_rpc_optional)):
|
||||||
|
if not strategy:
|
||||||
|
if not rpc:
|
||||||
|
raise RPCException("Strategy is mandatory in webserver mode.")
|
||||||
return PlotConfig.parse_obj(rpc._rpc_plot_config())
|
return PlotConfig.parse_obj(rpc._rpc_plot_config())
|
||||||
|
else:
|
||||||
|
config1 = deepcopy(config)
|
||||||
|
config1.update({
|
||||||
|
'strategy': strategy
|
||||||
|
})
|
||||||
|
return PlotConfig.parse_obj(RPC._rpc_plot_config_with_strategy(config1))
|
||||||
|
|
||||||
|
|
||||||
@router.get('/strategies', response_model=StrategyListResponse, tags=['strategy'])
|
@router.get('/strategies', response_model=StrategyListResponse, tags=['strategy'])
|
||||||
|
@ -673,6 +673,7 @@ class RPC:
|
|||||||
if self._freqtrade.state == State.RUNNING:
|
if self._freqtrade.state == State.RUNNING:
|
||||||
# Set 'max_open_trades' to 0
|
# Set 'max_open_trades' to 0
|
||||||
self._freqtrade.config['max_open_trades'] = 0
|
self._freqtrade.config['max_open_trades'] = 0
|
||||||
|
self._freqtrade.strategy.max_open_trades = 0
|
||||||
|
|
||||||
return {'status': 'No more entries will occur from now. Run /reload_config to reset.'}
|
return {'status': 'No more entries will occur from now. Run /reload_config to reset.'}
|
||||||
|
|
||||||
@ -944,7 +945,7 @@ class RPC:
|
|||||||
resp['errors'] = errors
|
resp['errors'] = errors
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def _rpc_blacklist(self, add: List[str] = None) -> Dict:
|
def _rpc_blacklist(self, add: Optional[List[str]] = None) -> Dict:
|
||||||
""" Returns the currently active blacklist"""
|
""" Returns the currently active blacklist"""
|
||||||
errors = {}
|
errors = {}
|
||||||
if add:
|
if add:
|
||||||
@ -1126,12 +1127,12 @@ class RPC:
|
|||||||
return self._freqtrade.active_pair_whitelist
|
return self._freqtrade.active_pair_whitelist
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _rpc_analysed_history_full(config, pair: str, timeframe: str,
|
def _rpc_analysed_history_full(config: Config, pair: str, timeframe: str,
|
||||||
timerange: str, exchange) -> Dict[str, Any]:
|
timerange: str, exchange) -> Dict[str, Any]:
|
||||||
timerange_parsed = TimeRange.parse_timerange(timerange)
|
timerange_parsed = TimeRange.parse_timerange(timerange)
|
||||||
|
|
||||||
_data = load_data(
|
_data = load_data(
|
||||||
datadir=config.get("datadir"),
|
datadir=config["datadir"],
|
||||||
pairs=[pair],
|
pairs=[pair],
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
timerange=timerange_parsed,
|
timerange=timerange_parsed,
|
||||||
@ -1156,6 +1157,16 @@ class RPC:
|
|||||||
self._freqtrade.strategy.plot_config['subplots'] = {}
|
self._freqtrade.strategy.plot_config['subplots'] = {}
|
||||||
return self._freqtrade.strategy.plot_config
|
return self._freqtrade.strategy.plot_config
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _rpc_plot_config_with_strategy(config: Config) -> Dict[str, Any]:
|
||||||
|
|
||||||
|
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||||
|
strategy = StrategyResolver.load_strategy(config)
|
||||||
|
|
||||||
|
if (strategy.plot_config and 'subplots' not in strategy.plot_config):
|
||||||
|
strategy.plot_config['subplots'] = {}
|
||||||
|
return strategy.plot_config
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _rpc_sysinfo() -> Dict[str, Any]:
|
def _rpc_sysinfo() -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
|
@ -1605,7 +1605,7 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
|
def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
|
||||||
disable_notification: bool = False,
|
disable_notification: bool = False,
|
||||||
keyboard: List[List[InlineKeyboardButton]] = None,
|
keyboard: Optional[List[List[InlineKeyboardButton]]] = None,
|
||||||
callback_path: str = "",
|
callback_path: str = "",
|
||||||
reload_able: bool = False,
|
reload_able: bool = False,
|
||||||
query: Optional[CallbackQuery] = None) -> None:
|
query: Optional[CallbackQuery] = None) -> None:
|
||||||
|
@ -4,7 +4,7 @@ This module defines a base class for auto-hyperoptable strategies.
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Iterator, List, Tuple, Type, Union
|
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
@ -36,7 +36,8 @@ class HyperStrategyMixin:
|
|||||||
self._ft_params_from_file = params
|
self._ft_params_from_file = params
|
||||||
# Init/loading of parameters is done as part of ft_bot_start().
|
# Init/loading of parameters is done as part of ft_bot_start().
|
||||||
|
|
||||||
def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]:
|
def enumerate_parameters(
|
||||||
|
self, category: Optional[str] = None) -> Iterator[Tuple[str, BaseParameter]]:
|
||||||
"""
|
"""
|
||||||
Find all optimizable parameters and return (name, attr) iterator.
|
Find all optimizable parameters and return (name, attr) iterator.
|
||||||
:param category:
|
:param category:
|
||||||
@ -80,6 +81,8 @@ class HyperStrategyMixin:
|
|||||||
|
|
||||||
self.stoploss = params.get('stoploss', {}).get(
|
self.stoploss = params.get('stoploss', {}).get(
|
||||||
'stoploss', getattr(self, 'stoploss', -0.1))
|
'stoploss', getattr(self, 'stoploss', -0.1))
|
||||||
|
self.max_open_trades = params.get('max_open_trades', {}).get(
|
||||||
|
'max_open_trades', getattr(self, 'max_open_trades', -1))
|
||||||
trailing = params.get('trailing', {})
|
trailing = params.get('trailing', {})
|
||||||
self.trailing_stop = trailing.get(
|
self.trailing_stop = trailing.get(
|
||||||
'trailing_stop', getattr(self, 'trailing_stop', False))
|
'trailing_stop', getattr(self, 'trailing_stop', False))
|
||||||
|
@ -10,7 +10,7 @@ from typing import Dict, List, Optional, Tuple, Union
|
|||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.constants import Config, ListPairsWithTimeframes
|
from freqtrade.constants import Config, IntOrInf, ListPairsWithTimeframes
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, RunMode, SignalDirection,
|
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, RunMode, SignalDirection,
|
||||||
SignalTagType, SignalType, TradingMode)
|
SignalTagType, SignalType, TradingMode)
|
||||||
@ -54,6 +54,9 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
# associated stoploss
|
# associated stoploss
|
||||||
stoploss: float
|
stoploss: float
|
||||||
|
|
||||||
|
# max open trades for the strategy
|
||||||
|
max_open_trades: IntOrInf
|
||||||
|
|
||||||
# trailing stoploss
|
# trailing stoploss
|
||||||
trailing_stop: bool = False
|
trailing_stop: bool = False
|
||||||
trailing_stop_positive: Optional[float] = None
|
trailing_stop_positive: Optional[float] = None
|
||||||
@ -595,7 +598,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def populate_any_indicators(self, pair: str, df: DataFrame, tf: str,
|
def populate_any_indicators(self, pair: str, df: DataFrame, tf: str,
|
||||||
informative: DataFrame = None,
|
informative: Optional[DataFrame] = None,
|
||||||
set_generalized_indicators: bool = False) -> DataFrame:
|
set_generalized_indicators: bool = False) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
DEPRECATED - USE FEATURE ENGINEERING FUNCTIONS INSTEAD
|
DEPRECATED - USE FEATURE ENGINEERING FUNCTIONS INSTEAD
|
||||||
@ -756,7 +759,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
||||||
def lock_pair(self, pair: str, until: datetime, reason: str = None, side: str = '*') -> None:
|
def lock_pair(self, pair: str, until: datetime,
|
||||||
|
reason: Optional[str] = None, side: str = '*') -> None:
|
||||||
"""
|
"""
|
||||||
Locks pair until a given timestamp happens.
|
Locks pair until a given timestamp happens.
|
||||||
Locked pairs are not analyzed, and are prevented from opening new trades.
|
Locked pairs are not analyzed, and are prevented from opening new trades.
|
||||||
@ -788,7 +792,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
PairLocks.unlock_reason(reason, datetime.now(timezone.utc))
|
PairLocks.unlock_reason(reason, datetime.now(timezone.utc))
|
||||||
|
|
||||||
def is_pair_locked(self, pair: str, *, candle_date: datetime = None, side: str = '*') -> bool:
|
def is_pair_locked(self, pair: str, *, candle_date: Optional[datetime] = None,
|
||||||
|
side: str = '*') -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if a pair is currently locked
|
Checks if a pair is currently locked
|
||||||
The 2nd, optional parameter ensures that locks are applied until the new candle arrives,
|
The 2nd, optional parameter ensures that locks are applied until the new candle arrives,
|
||||||
@ -959,7 +964,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
pair: str,
|
pair: str,
|
||||||
timeframe: str,
|
timeframe: str,
|
||||||
dataframe: DataFrame,
|
dataframe: DataFrame,
|
||||||
is_short: bool = None
|
is_short: Optional[bool] = None
|
||||||
) -> Tuple[bool, bool, Optional[str]]:
|
) -> Tuple[bool, bool, Optional[str]]:
|
||||||
"""
|
"""
|
||||||
Calculates current exit signal based based on the dataframe
|
Calculates current exit signal based based on the dataframe
|
||||||
@ -1058,7 +1063,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
|
|
||||||
def should_exit(self, trade: Trade, rate: float, current_time: datetime, *,
|
def should_exit(self, trade: Trade, rate: float, current_time: datetime, *,
|
||||||
enter: bool, exit_: bool,
|
enter: bool, exit_: bool,
|
||||||
low: float = None, high: float = None,
|
low: Optional[float] = None, high: Optional[float] = None,
|
||||||
force_stoploss: float = 0) -> List[ExitCheckTuple]:
|
force_stoploss: float = 0) -> List[ExitCheckTuple]:
|
||||||
"""
|
"""
|
||||||
This function evaluates if one of the conditions required to trigger an exit order
|
This function evaluates if one of the conditions required to trigger an exit order
|
||||||
@ -1146,8 +1151,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
|
|
||||||
def stop_loss_reached(self, current_rate: float, trade: Trade,
|
def stop_loss_reached(self, current_rate: float, trade: Trade,
|
||||||
current_time: datetime, current_profit: float,
|
current_time: datetime, current_profit: float,
|
||||||
force_stoploss: float, low: float = None,
|
force_stoploss: float, low: Optional[float] = None,
|
||||||
high: float = None) -> ExitCheckTuple:
|
high: Optional[float] = None) -> ExitCheckTuple:
|
||||||
"""
|
"""
|
||||||
Based on current profit of the trade and configured (trailing) stoploss,
|
Based on current profit of the trade and configured (trailing) stoploss,
|
||||||
decides to exit or not
|
decides to exit or not
|
||||||
|
@ -41,20 +41,6 @@
|
|||||||
"pairlists": [
|
"pairlists": [
|
||||||
{{ '{"method": "StaticPairList"}' if exchange_name == 'bittrex' else volume_pairlist }}
|
{{ '{"method": "StaticPairList"}' if exchange_name == 'bittrex' else volume_pairlist }}
|
||||||
],
|
],
|
||||||
"edge": {
|
|
||||||
"enabled": false,
|
|
||||||
"process_throttle_secs": 3600,
|
|
||||||
"calculate_since_number_of_days": 7,
|
|
||||||
"allowed_risk": 0.01,
|
|
||||||
"stoploss_range_min": -0.01,
|
|
||||||
"stoploss_range_max": -0.1,
|
|
||||||
"stoploss_range_step": -0.01,
|
|
||||||
"minimum_winrate": 0.60,
|
|
||||||
"minimum_expectancy": 0.20,
|
|
||||||
"min_trade_number": 10,
|
|
||||||
"max_trade_duration_minute": 1440,
|
|
||||||
"remove_pumps": false
|
|
||||||
},
|
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": {{ telegram | lower }},
|
"enabled": {{ telegram | lower }},
|
||||||
"token": "{{ telegram_token }}",
|
"token": "{{ telegram_token }}",
|
||||||
|
78
freqtrade/util/binance_mig.py
Normal file
78
freqtrade/util/binance_mig.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from packaging import version
|
||||||
|
|
||||||
|
from freqtrade.constants import Config
|
||||||
|
from freqtrade.enums.tradingmode import TradingMode
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.persistence.pairlock import PairLock
|
||||||
|
from freqtrade.persistence.trade_model import Trade
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_binance_futures_names(config: Config):
|
||||||
|
|
||||||
|
if (
|
||||||
|
not (config.get('trading_mode', TradingMode.SPOT) == TradingMode.FUTURES
|
||||||
|
and config['exchange']['name'] == 'binance')
|
||||||
|
):
|
||||||
|
# only act on new futures
|
||||||
|
return
|
||||||
|
import ccxt
|
||||||
|
if version.parse("2.6.26") > version.parse(ccxt.__version__):
|
||||||
|
raise OperationalException(
|
||||||
|
"Please follow the update instructions in the docs "
|
||||||
|
"(https://www.freqtrade.io/en/latest/updating/) to install a compatible ccxt version.")
|
||||||
|
_migrate_binance_futures_db(config)
|
||||||
|
migrate_binance_futures_data(config)
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_binance_futures_db(config: Config):
|
||||||
|
logger.warning('Migrating binance futures pairs in database.')
|
||||||
|
trades = Trade.get_trades([Trade.exchange == 'binance', Trade.trading_mode == 'FUTURES']).all()
|
||||||
|
for trade in trades:
|
||||||
|
if ':' in trade.pair:
|
||||||
|
# already migrated
|
||||||
|
continue
|
||||||
|
new_pair = f"{trade.pair}:{trade.stake_currency}"
|
||||||
|
trade.pair = new_pair
|
||||||
|
|
||||||
|
for order in trade.orders:
|
||||||
|
order.ft_pair = new_pair
|
||||||
|
# Should symbol be migrated too?
|
||||||
|
# order.symbol = new_pair
|
||||||
|
Trade.commit()
|
||||||
|
pls = PairLock.query.filter(PairLock.pair.notlike('%:%'))
|
||||||
|
for pl in pls:
|
||||||
|
pl.pair = f"{pl.pair}:{config['stake_currency']}"
|
||||||
|
# print(pls)
|
||||||
|
# pls.update({'pair': concat(PairLock.pair,':USDT')})
|
||||||
|
Trade.commit()
|
||||||
|
logger.warning('Done migrating binance futures pairs in database.')
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_binance_futures_data(config: Config):
|
||||||
|
|
||||||
|
if (
|
||||||
|
not (config.get('trading_mode', TradingMode.SPOT) == TradingMode.FUTURES
|
||||||
|
and config['exchange']['name'] == 'binance')
|
||||||
|
):
|
||||||
|
# only act on new futures
|
||||||
|
return
|
||||||
|
|
||||||
|
from freqtrade.data.history.idatahandler import get_datahandler
|
||||||
|
dhc = get_datahandler(config['datadir'], config.get('dataformat_ohlcv', 'json'))
|
||||||
|
|
||||||
|
paircombs = dhc.ohlcv_get_available_data(
|
||||||
|
config['datadir'],
|
||||||
|
config.get('trading_mode', TradingMode.SPOT)
|
||||||
|
)
|
||||||
|
|
||||||
|
for pair, timeframe, candle_type in paircombs:
|
||||||
|
if ':' in pair:
|
||||||
|
# already migrated
|
||||||
|
continue
|
||||||
|
new_pair = f"{pair}:{config['stake_currency']}"
|
||||||
|
dhc.rename_futures_data(pair, new_pair, timeframe, candle_type)
|
@ -297,16 +297,16 @@ class Wallets:
|
|||||||
logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.")
|
logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
max_stake_amount = min(max_stake_amount, self.get_available_stake_amount())
|
max_allowed_stake = min(max_stake_amount, self.get_available_stake_amount())
|
||||||
if trade_amount:
|
if trade_amount:
|
||||||
# if in a trade, then the resulting trade size cannot go beyond the max stake
|
# if in a trade, then the resulting trade size cannot go beyond the max stake
|
||||||
# Otherwise we could no longer exit.
|
# Otherwise we could no longer exit.
|
||||||
max_stake_amount = min(max_stake_amount, max_stake_amount - trade_amount)
|
max_allowed_stake = min(max_allowed_stake, max_stake_amount - trade_amount)
|
||||||
|
|
||||||
if min_stake_amount is not None and min_stake_amount > max_stake_amount:
|
if min_stake_amount is not None and min_stake_amount > max_allowed_stake:
|
||||||
if self._log:
|
if self._log:
|
||||||
logger.warning("Minimum stake amount > available balance. "
|
logger.warning("Minimum stake amount > available balance. "
|
||||||
f"{min_stake_amount} > {max_stake_amount}")
|
f"{min_stake_amount} > {max_allowed_stake}")
|
||||||
return 0
|
return 0
|
||||||
if min_stake_amount is not None and stake_amount < min_stake_amount:
|
if min_stake_amount is not None and stake_amount < min_stake_amount:
|
||||||
if self._log:
|
if self._log:
|
||||||
@ -325,11 +325,11 @@ class Wallets:
|
|||||||
return 0
|
return 0
|
||||||
stake_amount = min_stake_amount
|
stake_amount = min_stake_amount
|
||||||
|
|
||||||
if stake_amount > max_stake_amount:
|
if stake_amount > max_allowed_stake:
|
||||||
if self._log:
|
if self._log:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Stake amount for pair {pair} is too big "
|
f"Stake amount for pair {pair} is too big "
|
||||||
f"({stake_amount} > {max_stake_amount}), adjusting to {max_stake_amount}."
|
f"({stake_amount} > {max_allowed_stake}), adjusting to {max_allowed_stake}."
|
||||||
)
|
)
|
||||||
stake_amount = max_stake_amount
|
stake_amount = max_allowed_stake
|
||||||
return stake_amount
|
return stake_amount
|
||||||
|
@ -26,7 +26,7 @@ class Worker:
|
|||||||
Freqtradebot worker class
|
Freqtradebot worker class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args: Dict[str, Any], config: Config = None) -> None:
|
def __init__(self, args: Dict[str, Any], config: Optional[Config] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Init all variables and objects the bot needs to work
|
Init all variables and objects the bot needs to work
|
||||||
"""
|
"""
|
||||||
|
@ -31,7 +31,6 @@ asyncio_mode = "auto"
|
|||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
namespace_packages = false
|
namespace_packages = false
|
||||||
implicit_optional = true
|
|
||||||
warn_unused_ignores = true
|
warn_unused_ignores = true
|
||||||
exclude = [
|
exclude = [
|
||||||
'^build_helpers\.py$'
|
'^build_helpers\.py$'
|
||||||
@ -41,6 +40,11 @@ exclude = [
|
|||||||
module = "tests.*"
|
module = "tests.*"
|
||||||
ignore_errors = true
|
ignore_errors = true
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
# Telegram does not use implicit_optional = false in the current version.
|
||||||
|
module = "telegram.*"
|
||||||
|
implicit_optional = true
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools >= 46.4.0", "wheel"]
|
requires = ["setuptools >= 46.4.0", "wheel"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
@ -52,6 +56,3 @@ exclude = [
|
|||||||
"build_helpers/*.py",
|
"build_helpers/*.py",
|
||||||
]
|
]
|
||||||
ignore = ["freqtrade/vendor/**"]
|
ignore = ["freqtrade/vendor/**"]
|
||||||
|
|
||||||
# Align pyright to mypy config
|
|
||||||
strictParameterNoneValue = false
|
|
||||||
|
@ -11,7 +11,7 @@ flake8==6.0.0
|
|||||||
flake8-tidy-imports==4.8.0
|
flake8-tidy-imports==4.8.0
|
||||||
mypy==0.991
|
mypy==0.991
|
||||||
pre-commit==2.21.0
|
pre-commit==2.21.0
|
||||||
pytest==7.2.0
|
pytest==7.2.1
|
||||||
pytest-asyncio==0.20.3
|
pytest-asyncio==0.20.3
|
||||||
pytest-cov==4.0.0
|
pytest-cov==4.0.0
|
||||||
pytest-mock==3.10.0
|
pytest-mock==3.10.0
|
||||||
@ -23,11 +23,11 @@ time-machine==2.9.0
|
|||||||
httpx==0.23.3
|
httpx==0.23.3
|
||||||
|
|
||||||
# Convert jupyter notebooks to markdown documents
|
# Convert jupyter notebooks to markdown documents
|
||||||
nbconvert==7.2.7
|
nbconvert==7.2.8
|
||||||
|
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==5.2.1
|
types-cachetools==5.2.1
|
||||||
types-filelock==3.2.7
|
types-filelock==3.2.7
|
||||||
types-requests==2.28.11.7
|
types-requests==2.28.11.8
|
||||||
types-tabulate==0.9.0.0
|
types-tabulate==0.9.0.0
|
||||||
types-python-dateutil==2.8.19.5
|
types-python-dateutil==2.8.19.6
|
||||||
|
@ -7,5 +7,5 @@ scikit-learn==1.1.3
|
|||||||
joblib==1.2.0
|
joblib==1.2.0
|
||||||
catboost==1.1.1; platform_machine != 'aarch64'
|
catboost==1.1.1; platform_machine != 'aarch64'
|
||||||
lightgbm==3.3.4
|
lightgbm==3.3.4
|
||||||
xgboost==1.7.2
|
xgboost==1.7.3
|
||||||
tensorboard==2.11.0
|
tensorboard==2.11.2
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
numpy==1.24.1
|
numpy==1.24.1
|
||||||
pandas==1.5.2
|
pandas==1.5.3
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==2.5.56
|
ccxt==2.6.65
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==38.0.1; platform_machine == 'armv7l'
|
cryptography==38.0.1; platform_machine == 'armv7l'
|
||||||
cryptography==38.0.4; platform_machine != 'armv7l'
|
cryptography==39.0.0; platform_machine != 'armv7l'
|
||||||
aiohttp==3.8.3
|
aiohttp==3.8.3
|
||||||
SQLAlchemy==1.4.46
|
SQLAlchemy==1.4.46
|
||||||
python-telegram-bot==13.15
|
python-telegram-bot==13.15
|
||||||
arrow==1.2.3
|
arrow==1.2.3
|
||||||
cachetools==4.2.2
|
cachetools==4.2.2
|
||||||
requests==2.28.1
|
requests==2.28.2
|
||||||
urllib3==1.26.13
|
urllib3==1.26.14
|
||||||
jsonschema==4.17.3
|
jsonschema==4.17.3
|
||||||
TA-Lib==0.4.25
|
TA-Lib==0.4.25
|
||||||
technical==1.3.0
|
technical==1.3.0
|
||||||
@ -30,13 +30,13 @@ py_find_1st==1.1.5
|
|||||||
# Load ticker files 30% faster
|
# Load ticker files 30% faster
|
||||||
python-rapidjson==1.9
|
python-rapidjson==1.9
|
||||||
# Properly format api responses
|
# Properly format api responses
|
||||||
orjson==3.8.4
|
orjson==3.8.5
|
||||||
|
|
||||||
# Notify systemd
|
# Notify systemd
|
||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.89.0
|
fastapi==0.89.1
|
||||||
pydantic==1.10.4
|
pydantic==1.10.4
|
||||||
uvicorn==0.20.0
|
uvicorn==0.20.0
|
||||||
pyjwt==2.6.0
|
pyjwt==2.6.0
|
||||||
|
@ -14,6 +14,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
from urllib.parse import urlencode, urlparse, urlunparse
|
from urllib.parse import urlencode, urlparse, urlunparse
|
||||||
|
|
||||||
import rapidjson
|
import rapidjson
|
||||||
@ -36,7 +37,7 @@ class FtRestClient():
|
|||||||
self._session = requests.Session()
|
self._session = requests.Session()
|
||||||
self._session.auth = (username, password)
|
self._session.auth = (username, password)
|
||||||
|
|
||||||
def _call(self, method, apipath, params: dict = None, data=None, files=None):
|
def _call(self, method, apipath, params: Optional[dict] = None, data=None, files=None):
|
||||||
|
|
||||||
if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'):
|
if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'):
|
||||||
raise ValueError(f'invalid method <{method}>')
|
raise ValueError(f'invalid method <{method}>')
|
||||||
@ -60,13 +61,13 @@ class FtRestClient():
|
|||||||
except ConnectionError:
|
except ConnectionError:
|
||||||
logger.warning("Connection error")
|
logger.warning("Connection error")
|
||||||
|
|
||||||
def _get(self, apipath, params: dict = None):
|
def _get(self, apipath, params: Optional[dict] = None):
|
||||||
return self._call("GET", apipath, params=params)
|
return self._call("GET", apipath, params=params)
|
||||||
|
|
||||||
def _delete(self, apipath, params: dict = None):
|
def _delete(self, apipath, params: Optional[dict] = None):
|
||||||
return self._call("DELETE", apipath, params=params)
|
return self._call("DELETE", apipath, params=params)
|
||||||
|
|
||||||
def _post(self, apipath, params: dict = None, data: dict = None):
|
def _post(self, apipath, params: Optional[dict] = None, data: Optional[dict] = None):
|
||||||
return self._call("POST", apipath, params=params, data=data)
|
return self._call("POST", apipath, params=params, data=data)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
2
setup.py
2
setup.py
@ -60,7 +60,7 @@ setup(
|
|||||||
],
|
],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
# from requirements.txt
|
# from requirements.txt
|
||||||
'ccxt>=1.92.9',
|
'ccxt>=2.6.26',
|
||||||
'SQLAlchemy',
|
'SQLAlchemy',
|
||||||
'python-telegram-bot>=13.4',
|
'python-telegram-bot>=13.4',
|
||||||
'arrow>=0.17.0',
|
'arrow>=0.17.0',
|
||||||
|
@ -1450,10 +1450,10 @@ def test_start_list_data(testdatadir, capsys):
|
|||||||
start_list_data(pargs)
|
start_list_data(pargs)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
||||||
assert "Found 5 pair / timeframe combinations." in captured.out
|
assert "Found 6 pair / timeframe combinations." in captured.out
|
||||||
assert "\n| Pair | Timeframe | Type |\n" in captured.out
|
assert "\n| Pair | Timeframe | Type |\n" in captured.out
|
||||||
assert "\n| XRP/USDT | 1h | futures |\n" in captured.out
|
assert "\n| XRP/USDT:USDT | 5m, 1h | futures |\n" in captured.out
|
||||||
assert "\n| XRP/USDT | 1h, 8h | mark |\n" in captured.out
|
assert "\n| XRP/USDT:USDT | 1h, 8h | mark |\n" in captured.out
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
"list-data",
|
"list-data",
|
||||||
|
@ -241,7 +241,6 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
|||||||
:return: FreqtradeBot
|
:return: FreqtradeBot
|
||||||
"""
|
"""
|
||||||
patch_freqtradebot(mocker, config)
|
patch_freqtradebot(mocker, config)
|
||||||
config['datadir'] = Path(config['datadir'])
|
|
||||||
return FreqtradeBot(config)
|
return FreqtradeBot(config)
|
||||||
|
|
||||||
|
|
||||||
@ -510,7 +509,7 @@ def get_default_conf(testdatadir):
|
|||||||
"chat_id": "0",
|
"chat_id": "0",
|
||||||
"notification_settings": {},
|
"notification_settings": {},
|
||||||
},
|
},
|
||||||
"datadir": str(testdatadir),
|
"datadir": Path(testdatadir),
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"db_url": "sqlite://",
|
"db_url": "sqlite://",
|
||||||
"user_data_dir": Path("user_data"),
|
"user_data_dir": Path("user_data"),
|
||||||
@ -3109,7 +3108,7 @@ def funding_rate_history_octohourly():
|
|||||||
@pytest.fixture(scope='function')
|
@pytest.fixture(scope='function')
|
||||||
def leverage_tiers():
|
def leverage_tiers():
|
||||||
return {
|
return {
|
||||||
"1000SHIB/USDT": [
|
"1000SHIB/USDT:USDT": [
|
||||||
{
|
{
|
||||||
'minNotional': 0,
|
'minNotional': 0,
|
||||||
'maxNotional': 50000,
|
'maxNotional': 50000,
|
||||||
@ -3160,7 +3159,7 @@ def leverage_tiers():
|
|||||||
'maintAmt': 654500.0
|
'maintAmt': 654500.0
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"1INCH/USDT": [
|
"1INCH/USDT:USDT": [
|
||||||
{
|
{
|
||||||
'minNotional': 0,
|
'minNotional': 0,
|
||||||
'maxNotional': 5000,
|
'maxNotional': 5000,
|
||||||
@ -3204,7 +3203,7 @@ def leverage_tiers():
|
|||||||
'maintAmt': 386940.0
|
'maintAmt': 386940.0
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"AAVE/USDT": [
|
"AAVE/USDT:USDT": [
|
||||||
{
|
{
|
||||||
'minNotional': 0,
|
'minNotional': 0,
|
||||||
'maxNotional': 5000,
|
'maxNotional': 5000,
|
||||||
@ -3248,7 +3247,7 @@ def leverage_tiers():
|
|||||||
'maintAmt': 386950.0
|
'maintAmt': 386950.0
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"ADA/BUSD": [
|
"ADA/BUSD:BUSD": [
|
||||||
{
|
{
|
||||||
"minNotional": 0,
|
"minNotional": 0,
|
||||||
"maxNotional": 100000,
|
"maxNotional": 100000,
|
||||||
@ -3292,7 +3291,7 @@ def leverage_tiers():
|
|||||||
"maintAmt": 1527500.0
|
"maintAmt": 1527500.0
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'BNB/BUSD': [
|
'BNB/BUSD:BUSD': [
|
||||||
{
|
{
|
||||||
"minNotional": 0, # stake(before leverage) = 0
|
"minNotional": 0, # stake(before leverage) = 0
|
||||||
"maxNotional": 100000, # max stake(before leverage) = 5000
|
"maxNotional": 100000, # max stake(before leverage) = 5000
|
||||||
@ -3336,7 +3335,7 @@ def leverage_tiers():
|
|||||||
"maintAmt": 1527500.0
|
"maintAmt": 1527500.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
'BNB/USDT': [
|
'BNB/USDT:USDT': [
|
||||||
{
|
{
|
||||||
"minNotional": 0, # stake = 0.0
|
"minNotional": 0, # stake = 0.0
|
||||||
"maxNotional": 10000, # max_stake = 133.33333333333334
|
"maxNotional": 10000, # max_stake = 133.33333333333334
|
||||||
@ -3401,7 +3400,7 @@ def leverage_tiers():
|
|||||||
"maintAmt": 6233035.0
|
"maintAmt": 6233035.0
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'BTC/USDT': [
|
'BTC/USDT:USDT': [
|
||||||
{
|
{
|
||||||
"minNotional": 0, # stake = 0.0
|
"minNotional": 0, # stake = 0.0
|
||||||
"maxNotional": 50000, # max_stake = 400.0
|
"maxNotional": 50000, # max_stake = 400.0
|
||||||
@ -3473,7 +3472,7 @@ def leverage_tiers():
|
|||||||
"maintAmt": 1.997038E8
|
"maintAmt": 1.997038E8
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"ZEC/USDT": [
|
"ZEC/USDT:USDT": [
|
||||||
{
|
{
|
||||||
'minNotional': 0,
|
'minNotional': 0,
|
||||||
'maxNotional': 50000,
|
'maxNotional': 50000,
|
||||||
|
@ -294,8 +294,8 @@ def test_convert_trades_format(default_conf, testdatadir, tmpdir):
|
|||||||
|
|
||||||
@pytest.mark.parametrize('file_base,candletype', [
|
@pytest.mark.parametrize('file_base,candletype', [
|
||||||
(['XRP_ETH-5m', 'XRP_ETH-1m'], CandleType.SPOT),
|
(['XRP_ETH-5m', 'XRP_ETH-1m'], CandleType.SPOT),
|
||||||
(['UNITTEST_USDT-1h-mark', 'XRP_USDT-1h-mark'], CandleType.MARK),
|
(['UNITTEST_USDT_USDT-1h-mark', 'XRP_USDT_USDT-1h-mark'], CandleType.MARK),
|
||||||
(['XRP_USDT-1h-futures'], CandleType.FUTURES),
|
(['XRP_USDT_USDT-1h-futures'], CandleType.FUTURES),
|
||||||
])
|
])
|
||||||
def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir, file_base, candletype):
|
def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir, file_base, candletype):
|
||||||
tmpdir1 = Path(tmpdir)
|
tmpdir1 = Path(tmpdir)
|
||||||
@ -315,7 +315,10 @@ def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir, file_base, cand
|
|||||||
files_new.append(file_new)
|
files_new.append(file_new)
|
||||||
|
|
||||||
default_conf['datadir'] = tmpdir1
|
default_conf['datadir'] = tmpdir1
|
||||||
default_conf['pairs'] = ['XRP_ETH', 'XRP_USDT', 'UNITTEST_USDT']
|
if candletype == CandleType.SPOT:
|
||||||
|
default_conf['pairs'] = ['XRP/ETH', 'XRP/USDT', 'UNITTEST/USDT']
|
||||||
|
else:
|
||||||
|
default_conf['pairs'] = ['XRP/ETH:ETH', 'XRP/USDT:USDT', 'UNITTEST/USDT:USDT']
|
||||||
default_conf['timeframes'] = ['1m', '5m', '1h']
|
default_conf['timeframes'] = ['1m', '5m', '1h']
|
||||||
|
|
||||||
assert not file_new.exists()
|
assert not file_new.exists()
|
||||||
|
@ -33,10 +33,10 @@ def test_datahandler_ohlcv_get_pairs(testdatadir):
|
|||||||
assert set(pairs) == {'UNITTEST/BTC'}
|
assert set(pairs) == {'UNITTEST/BTC'}
|
||||||
|
|
||||||
pairs = JsonDataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.MARK)
|
pairs = JsonDataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.MARK)
|
||||||
assert set(pairs) == {'UNITTEST/USDT', 'XRP/USDT'}
|
assert set(pairs) == {'UNITTEST/USDT:USDT', 'XRP/USDT:USDT'}
|
||||||
|
|
||||||
pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.FUTURES)
|
pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.FUTURES)
|
||||||
assert set(pairs) == {'XRP/USDT'}
|
assert set(pairs) == {'XRP/USDT:USDT'}
|
||||||
|
|
||||||
pairs = HDF5DataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.MARK)
|
pairs = HDF5DataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.MARK)
|
||||||
assert set(pairs) == {'UNITTEST/USDT:USDT'}
|
assert set(pairs) == {'UNITTEST/USDT:USDT'}
|
||||||
@ -104,11 +104,12 @@ def test_datahandler_ohlcv_get_available_data(testdatadir):
|
|||||||
paircombs = JsonDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.FUTURES)
|
paircombs = JsonDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.FUTURES)
|
||||||
# Convert to set to avoid failures due to sorting
|
# Convert to set to avoid failures due to sorting
|
||||||
assert set(paircombs) == {
|
assert set(paircombs) == {
|
||||||
('UNITTEST/USDT', '1h', 'mark'),
|
('UNITTEST/USDT:USDT', '1h', 'mark'),
|
||||||
('XRP/USDT', '1h', 'futures'),
|
('XRP/USDT:USDT', '5m', 'futures'),
|
||||||
('XRP/USDT', '1h', 'mark'),
|
('XRP/USDT:USDT', '1h', 'futures'),
|
||||||
('XRP/USDT', '8h', 'mark'),
|
('XRP/USDT:USDT', '1h', 'mark'),
|
||||||
('XRP/USDT', '8h', 'funding_rate'),
|
('XRP/USDT:USDT', '8h', 'mark'),
|
||||||
|
('XRP/USDT:USDT', '8h', 'funding_rate'),
|
||||||
}
|
}
|
||||||
|
|
||||||
paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.SPOT)
|
paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.SPOT)
|
||||||
@ -142,7 +143,7 @@ def test_jsondatahandler_ohlcv_load(testdatadir, caplog):
|
|||||||
df = dh.ohlcv_load('XRP/ETH', '5m', 'spot')
|
df = dh.ohlcv_load('XRP/ETH', '5m', 'spot')
|
||||||
assert len(df) == 712
|
assert len(df) == 712
|
||||||
|
|
||||||
df_mark = dh.ohlcv_load('UNITTEST/USDT', '1h', candle_type="mark")
|
df_mark = dh.ohlcv_load('UNITTEST/USDT:USDT', '1h', candle_type="mark")
|
||||||
assert len(df_mark) == 100
|
assert len(df_mark) == 100
|
||||||
|
|
||||||
df_no_mark = dh.ohlcv_load('UNITTEST/USDT', '1h', 'spot')
|
df_no_mark = dh.ohlcv_load('UNITTEST/USDT', '1h', 'spot')
|
||||||
@ -424,7 +425,7 @@ def test_hdf5datahandler_ohlcv_load_and_resave(
|
|||||||
# Data goes from 2018-01-10 - 2018-01-30
|
# Data goes from 2018-01-10 - 2018-01-30
|
||||||
('UNITTEST/BTC', '5m', 'spot', '', '2018-01-15', '2018-01-19'),
|
('UNITTEST/BTC', '5m', 'spot', '', '2018-01-15', '2018-01-19'),
|
||||||
# Mark data goes from to 2021-11-15 2021-11-19
|
# Mark data goes from to 2021-11-15 2021-11-19
|
||||||
('UNITTEST/USDT', '1h', 'mark', '-mark', '2021-11-16', '2021-11-18'),
|
('UNITTEST/USDT:USDT', '1h', 'mark', '-mark', '2021-11-16', '2021-11-18'),
|
||||||
])
|
])
|
||||||
@pytest.mark.parametrize('datahandler', ['hdf5', 'feather', 'parquet'])
|
@pytest.mark.parametrize('datahandler', ['hdf5', 'feather', 'parquet'])
|
||||||
def test_generic_datahandler_ohlcv_load_and_resave(
|
def test_generic_datahandler_ohlcv_load_and_resave(
|
||||||
|
@ -190,6 +190,15 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmp
|
|||||||
assert '1' in captured.out
|
assert '1' in captured.out
|
||||||
assert '2.5' in captured.out
|
assert '2.5' in captured.out
|
||||||
|
|
||||||
|
# test group 5
|
||||||
|
args = get_args(base_args + ['--analysis-groups', "5"])
|
||||||
|
start_analysis_entries_exits(args)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert 'exit_signal' in captured.out
|
||||||
|
assert 'roi' in captured.out
|
||||||
|
assert 'stop_loss' in captured.out
|
||||||
|
assert 'trailing_stop_loss' in captured.out
|
||||||
|
|
||||||
# test date filtering
|
# test date filtering
|
||||||
args = get_args(base_args + ['--timerange', "20180129-20180130"])
|
args = get_args(base_args + ['--timerange', "20180129-20180130"])
|
||||||
start_analysis_entries_exits(args)
|
start_analysis_entries_exits(args)
|
||||||
|
@ -78,11 +78,11 @@ def test_load_data_1min_timeframe(ohlcv_history, mocker, caplog, testdatadir) ->
|
|||||||
|
|
||||||
def test_load_data_mark(ohlcv_history, mocker, caplog, testdatadir) -> None:
|
def test_load_data_mark(ohlcv_history, mocker, caplog, testdatadir) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history)
|
||||||
file = testdatadir / 'futures/UNITTEST_USDT-1h-mark.json'
|
file = testdatadir / 'futures/UNITTEST_USDT_USDT-1h-mark.json'
|
||||||
load_data(datadir=testdatadir, timeframe='1h', pairs=['UNITTEST/BTC'], candle_type='mark')
|
load_data(datadir=testdatadir, timeframe='1h', pairs=['UNITTEST/BTC'], candle_type='mark')
|
||||||
assert file.is_file()
|
assert file.is_file()
|
||||||
assert not log_has(
|
assert not log_has(
|
||||||
'Download history data for pair: "UNITTEST/USDT", interval: 1m '
|
'Download history data for pair: "UNITTEST/USDT:USDT", interval: 1m '
|
||||||
'and store in None.', caplog
|
'and store in None.', caplog
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -575,25 +575,13 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, c
|
|||||||
assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog)
|
assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("trading_mode,margin_mode,config", [
|
|
||||||
("spot", "", {}),
|
|
||||||
("margin", "cross", {"options": {"defaultType": "margin"}}),
|
|
||||||
("futures", "isolated", {"options": {"defaultType": "future"}}),
|
|
||||||
])
|
|
||||||
def test__ccxt_config(default_conf, mocker, trading_mode, margin_mode, config):
|
|
||||||
default_conf['trading_mode'] = trading_mode
|
|
||||||
default_conf['margin_mode'] = margin_mode
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
|
||||||
assert exchange._ccxt_config == config
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('pair,nominal_value,mm_ratio,amt', [
|
@pytest.mark.parametrize('pair,nominal_value,mm_ratio,amt', [
|
||||||
("BNB/BUSD", 0.0, 0.025, 0),
|
("BNB/BUSD:BUSD", 0.0, 0.025, 0),
|
||||||
("BNB/USDT", 100.0, 0.0065, 0),
|
("BNB/USDT:USDT", 100.0, 0.0065, 0),
|
||||||
("BTC/USDT", 170.30, 0.004, 0),
|
("BTC/USDT:USDT", 170.30, 0.004, 0),
|
||||||
("BNB/BUSD", 999999.9, 0.1, 27500.0),
|
("BNB/BUSD:BUSD", 999999.9, 0.1, 27500.0),
|
||||||
("BNB/USDT", 5000000.0, 0.15, 233035.0),
|
("BNB/USDT:USDT", 5000000.0, 0.15, 233035.0),
|
||||||
("BTC/USDT", 600000000, 0.5, 1.997038E8),
|
("BTC/USDT:USDT", 600000000, 0.5, 1.997038E8),
|
||||||
])
|
])
|
||||||
def test_get_maintenance_ratio_and_amt_binance(
|
def test_get_maintenance_ratio_and_amt_binance(
|
||||||
default_conf,
|
default_conf,
|
||||||
|
@ -12,6 +12,7 @@ from typing import Tuple
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.constants import Config
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
||||||
from freqtrade.exchange.exchange import Exchange, timeframe_to_msecs
|
from freqtrade.exchange.exchange import Exchange, timeframe_to_msecs
|
||||||
@ -31,15 +32,36 @@ EXCHANGES = {
|
|||||||
'leverage_tiers_public': False,
|
'leverage_tiers_public': False,
|
||||||
'leverage_in_spot_market': False,
|
'leverage_in_spot_market': False,
|
||||||
},
|
},
|
||||||
# 'binance': {
|
'binance': {
|
||||||
# 'pair': 'BTC/USDT',
|
'pair': 'BTC/USDT',
|
||||||
# 'stake_currency': 'USDT',
|
'stake_currency': 'USDT',
|
||||||
# 'hasQuoteVolume': True,
|
'use_ci_proxy': True,
|
||||||
# 'timeframe': '5m',
|
'hasQuoteVolume': True,
|
||||||
# 'futures': True,
|
'timeframe': '5m',
|
||||||
# 'leverage_tiers_public': False,
|
'futures': True,
|
||||||
# 'leverage_in_spot_market': False,
|
'futures_pair': 'BTC/USDT:USDT',
|
||||||
# },
|
'hasQuoteVolumeFutures': True,
|
||||||
|
'leverage_tiers_public': False,
|
||||||
|
'leverage_in_spot_market': False,
|
||||||
|
'sample_order': {
|
||||||
|
"symbol": "SOLUSDT",
|
||||||
|
"orderId": 3551312894,
|
||||||
|
"orderListId": -1,
|
||||||
|
"clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba",
|
||||||
|
"transactTime": 1674493798550,
|
||||||
|
"price": "15.00000000",
|
||||||
|
"origQty": "1.00000000",
|
||||||
|
"executedQty": "0.00000000",
|
||||||
|
"cummulativeQuoteQty": "0.00000000",
|
||||||
|
"status": "NEW",
|
||||||
|
"timeInForce": "GTC",
|
||||||
|
"type": "LIMIT",
|
||||||
|
"side": "BUY",
|
||||||
|
"workingTime": 1674493798550,
|
||||||
|
"fills": [],
|
||||||
|
"selfTradePreventionMode": "NONE",
|
||||||
|
}
|
||||||
|
},
|
||||||
'kraken': {
|
'kraken': {
|
||||||
'pair': 'BTC/USDT',
|
'pair': 'BTC/USDT',
|
||||||
'stake_currency': 'USDT',
|
'stake_currency': 'USDT',
|
||||||
@ -63,6 +85,7 @@ EXCHANGES = {
|
|||||||
'timeframe': '5m',
|
'timeframe': '5m',
|
||||||
'futures': True,
|
'futures': True,
|
||||||
'futures_pair': 'BTC/USDT:USDT',
|
'futures_pair': 'BTC/USDT:USDT',
|
||||||
|
'hasQuoteVolumeFutures': True,
|
||||||
'leverage_tiers_public': True,
|
'leverage_tiers_public': True,
|
||||||
'leverage_in_spot_market': True,
|
'leverage_in_spot_market': True,
|
||||||
},
|
},
|
||||||
@ -71,8 +94,9 @@ EXCHANGES = {
|
|||||||
'stake_currency': 'USDT',
|
'stake_currency': 'USDT',
|
||||||
'hasQuoteVolume': True,
|
'hasQuoteVolume': True,
|
||||||
'timeframe': '5m',
|
'timeframe': '5m',
|
||||||
'futures_pair': 'BTC/USDT:USDT',
|
|
||||||
'futures': True,
|
'futures': True,
|
||||||
|
'futures_pair': 'BTC/USDT:USDT',
|
||||||
|
'hasQuoteVolumeFutures': False,
|
||||||
'leverage_tiers_public': True,
|
'leverage_tiers_public': True,
|
||||||
'leverage_in_spot_market': True,
|
'leverage_in_spot_market': True,
|
||||||
},
|
},
|
||||||
@ -106,8 +130,27 @@ def exchange_conf():
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def set_test_proxy(config: Config, use_proxy: bool) -> Config:
|
||||||
|
# Set proxy to test in CI.
|
||||||
|
import os
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config1
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=EXCHANGES, scope="class")
|
@pytest.fixture(params=EXCHANGES, scope="class")
|
||||||
def exchange(request, exchange_conf):
|
def exchange(request, exchange_conf):
|
||||||
|
exchange_conf = set_test_proxy(
|
||||||
|
exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False))
|
||||||
exchange_conf['exchange']['name'] = request.param
|
exchange_conf['exchange']['name'] = request.param
|
||||||
exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency']
|
exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency']
|
||||||
exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True)
|
exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True)
|
||||||
@ -120,6 +163,8 @@ def exchange_futures(request, exchange_conf, class_mocker):
|
|||||||
if not EXCHANGES[request.param].get('futures') is True:
|
if not EXCHANGES[request.param].get('futures') is True:
|
||||||
yield None, request.param
|
yield None, request.param
|
||||||
else:
|
else:
|
||||||
|
exchange_conf = set_test_proxy(
|
||||||
|
exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False))
|
||||||
exchange_conf = deepcopy(exchange_conf)
|
exchange_conf = deepcopy(exchange_conf)
|
||||||
exchange_conf['exchange']['name'] = request.param
|
exchange_conf['exchange']['name'] = request.param
|
||||||
exchange_conf['trading_mode'] = 'futures'
|
exchange_conf['trading_mode'] = 'futures'
|
||||||
@ -184,6 +229,19 @@ class TestCCXTExchange():
|
|||||||
|
|
||||||
assert exchange.market_is_future(markets[pair])
|
assert exchange.market_is_future(markets[pair])
|
||||||
|
|
||||||
|
def test_ccxt_order_parse(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||||
|
exch, exchange_name = exchange
|
||||||
|
if stuff := EXCHANGES[exchange_name].get('sample_order'):
|
||||||
|
|
||||||
|
po = exch._api.parse_order(stuff)
|
||||||
|
assert po['timestamp'] == 1674493798550
|
||||||
|
assert isinstance(po['timestamp'], int)
|
||||||
|
assert isinstance(po['price'], float)
|
||||||
|
assert isinstance(po['amount'], float)
|
||||||
|
assert isinstance(po['status'], str)
|
||||||
|
else:
|
||||||
|
pytest.skip(f"No sample order available for exchange {exchange_name}")
|
||||||
|
|
||||||
def test_ccxt_fetch_tickers(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
def test_ccxt_fetch_tickers(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||||
exch, exchangename = exchange
|
exch, exchangename = exchange
|
||||||
pair = EXCHANGES[exchangename]['pair']
|
pair = EXCHANGES[exchangename]['pair']
|
||||||
@ -198,6 +256,25 @@ class TestCCXTExchange():
|
|||||||
if EXCHANGES[exchangename].get('hasQuoteVolume'):
|
if EXCHANGES[exchangename].get('hasQuoteVolume'):
|
||||||
assert tickers[pair]['quoteVolume'] is not None
|
assert tickers[pair]['quoteVolume'] is not None
|
||||||
|
|
||||||
|
def test_ccxt_fetch_tickers_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
|
exch, exchangename = exchange_futures
|
||||||
|
if not exch or exchangename in ('gateio'):
|
||||||
|
# exchange_futures only returns values for supported exchanges
|
||||||
|
return
|
||||||
|
|
||||||
|
pair = EXCHANGES[exchangename]['pair']
|
||||||
|
pair = EXCHANGES[exchangename].get('futures_pair', pair)
|
||||||
|
|
||||||
|
tickers = exch.get_tickers()
|
||||||
|
assert pair in tickers
|
||||||
|
assert 'ask' in tickers[pair]
|
||||||
|
assert tickers[pair]['ask'] is not None
|
||||||
|
assert 'bid' in tickers[pair]
|
||||||
|
assert tickers[pair]['bid'] is not None
|
||||||
|
assert 'quoteVolume' in tickers[pair]
|
||||||
|
if EXCHANGES[exchangename].get('hasQuoteVolumeFutures'):
|
||||||
|
assert tickers[pair]['quoteVolume'] is not None
|
||||||
|
|
||||||
def test_ccxt_fetch_ticker(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
def test_ccxt_fetch_ticker(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||||
exch, exchangename = exchange
|
exch, exchangename = exchange
|
||||||
pair = EXCHANGES[exchangename]['pair']
|
pair = EXCHANGES[exchangename]['pair']
|
||||||
|
@ -3957,7 +3957,7 @@ def test_validate_trading_mode_and_margin_mode(
|
|||||||
@pytest.mark.parametrize("exchange_name,trading_mode,ccxt_config", [
|
@pytest.mark.parametrize("exchange_name,trading_mode,ccxt_config", [
|
||||||
("binance", "spot", {}),
|
("binance", "spot", {}),
|
||||||
("binance", "margin", {"options": {"defaultType": "margin"}}),
|
("binance", "margin", {"options": {"defaultType": "margin"}}),
|
||||||
("binance", "futures", {"options": {"defaultType": "future"}}),
|
("binance", "futures", {"options": {"defaultType": "swap"}}),
|
||||||
("bybit", "spot", {"options": {"defaultType": "spot"}}),
|
("bybit", "spot", {"options": {"defaultType": "spot"}}),
|
||||||
("bybit", "futures", {"options": {"defaultType": "linear"}}),
|
("bybit", "futures", {"options": {"defaultType": "linear"}}),
|
||||||
("gateio", "futures", {"options": {"defaultType": "swap"}}),
|
("gateio", "futures", {"options": {"defaultType": "swap"}}),
|
||||||
@ -4898,22 +4898,22 @@ def test_get_maintenance_ratio_and_amt_exceptions(mocker, default_conf, leverage
|
|||||||
OperationalException,
|
OperationalException,
|
||||||
match='nominal value can not be lower than 0',
|
match='nominal value can not be lower than 0',
|
||||||
):
|
):
|
||||||
exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT', -1)
|
exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT:USDT', -1)
|
||||||
|
|
||||||
exchange._leverage_tiers = {}
|
exchange._leverage_tiers = {}
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
InvalidOrderException,
|
InvalidOrderException,
|
||||||
match="Maintenance margin rate for 1000SHIB/USDT is unavailable for",
|
match="Maintenance margin rate for 1000SHIB/USDT:USDT is unavailable for",
|
||||||
):
|
):
|
||||||
exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT', 10000)
|
exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT:USDT', 10000)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('pair,value,mmr,maintAmt', [
|
@pytest.mark.parametrize('pair,value,mmr,maintAmt', [
|
||||||
('ADA/BUSD', 500, 0.025, 0.0),
|
('ADA/BUSD:BUSD', 500, 0.025, 0.0),
|
||||||
('ADA/BUSD', 20000000, 0.5, 1527500.0),
|
('ADA/BUSD:BUSD', 20000000, 0.5, 1527500.0),
|
||||||
('ZEC/USDT', 500, 0.01, 0.0),
|
('ZEC/USDT:USDT', 500, 0.01, 0.0),
|
||||||
('ZEC/USDT', 20000000, 0.5, 654500.0),
|
('ZEC/USDT:USDT', 20000000, 0.5, 654500.0),
|
||||||
])
|
])
|
||||||
def test_get_maintenance_ratio_and_amt(
|
def test_get_maintenance_ratio_and_amt(
|
||||||
mocker,
|
mocker,
|
||||||
@ -4946,21 +4946,21 @@ def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers):
|
|||||||
|
|
||||||
exchange._leverage_tiers = leverage_tiers
|
exchange._leverage_tiers = leverage_tiers
|
||||||
|
|
||||||
assert exchange.get_max_leverage("BNB/BUSD", 1.0) == 20.0
|
assert exchange.get_max_leverage("BNB/BUSD:BUSD", 1.0) == 20.0
|
||||||
assert exchange.get_max_leverage("BNB/USDT", 100.0) == 75.0
|
assert exchange.get_max_leverage("BNB/USDT:USDT", 100.0) == 75.0
|
||||||
assert exchange.get_max_leverage("BTC/USDT", 170.30) == 125.0
|
assert exchange.get_max_leverage("BTC/USDT:USDT", 170.30) == 125.0
|
||||||
assert pytest.approx(exchange.get_max_leverage("BNB/BUSD", 99999.9)) == 5.000005
|
assert pytest.approx(exchange.get_max_leverage("BNB/BUSD:BUSD", 99999.9)) == 5.000005
|
||||||
assert pytest.approx(exchange.get_max_leverage("BNB/USDT", 1500)) == 33.333333333333333
|
assert pytest.approx(exchange.get_max_leverage("BNB/USDT:USDT", 1500)) == 33.333333333333333
|
||||||
assert exchange.get_max_leverage("BTC/USDT", 300000000) == 2.0
|
assert exchange.get_max_leverage("BTC/USDT:USDT", 300000000) == 2.0
|
||||||
assert exchange.get_max_leverage("BTC/USDT", 600000000) == 1.0 # Last tier
|
assert exchange.get_max_leverage("BTC/USDT:USDT", 600000000) == 1.0 # Last tier
|
||||||
|
|
||||||
assert exchange.get_max_leverage("SPONGE/USDT", 200) == 1.0 # Pair not in leverage_tiers
|
assert exchange.get_max_leverage("SPONGE/USDT:USDT", 200) == 1.0 # Pair not in leverage_tiers
|
||||||
assert exchange.get_max_leverage("BTC/USDT", 0.0) == 125.0 # No stake amount
|
assert exchange.get_max_leverage("BTC/USDT:USDT", 0.0) == 125.0 # No stake amount
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
InvalidOrderException,
|
InvalidOrderException,
|
||||||
match=r'Amount 1000000000.01 too high for BTC/USDT'
|
match=r'Amount 1000000000.01 too high for BTC/USDT:USDT'
|
||||||
):
|
):
|
||||||
exchange.get_max_leverage("BTC/USDT", 1000000000.01)
|
exchange.get_max_leverage("BTC/USDT:USDT", 1000000000.01)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", ['bittrex', 'binance', 'kraken', 'gateio', 'okx'])
|
@pytest.mark.parametrize("exchange_name", ['bittrex', 'binance', 'kraken', 'gateio', 'okx'])
|
||||||
|
@ -195,12 +195,12 @@ def test_get_max_pair_stake_amount_okx(default_conf, mocker, leverage_tiers):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf, id="okx")
|
exchange = get_patched_exchange(mocker, default_conf, id="okx")
|
||||||
exchange._leverage_tiers = leverage_tiers
|
exchange._leverage_tiers = leverage_tiers
|
||||||
|
|
||||||
assert exchange.get_max_pair_stake_amount('BNB/BUSD', 1.0) == 30000000
|
assert exchange.get_max_pair_stake_amount('BNB/BUSD:BUSD', 1.0) == 30000000
|
||||||
assert exchange.get_max_pair_stake_amount('BNB/USDT', 1.0) == 50000000
|
assert exchange.get_max_pair_stake_amount('BNB/USDT:USDT', 1.0) == 50000000
|
||||||
assert exchange.get_max_pair_stake_amount('BTC/USDT', 1.0) == 1000000000
|
assert exchange.get_max_pair_stake_amount('BTC/USDT:USDT', 1.0) == 1000000000
|
||||||
assert exchange.get_max_pair_stake_amount('BTC/USDT', 1.0, 10.0) == 100000000
|
assert exchange.get_max_pair_stake_amount('BTC/USDT:USDT', 1.0, 10.0) == 100000000
|
||||||
|
|
||||||
assert exchange.get_max_pair_stake_amount('TTT/USDT', 1.0) == float('inf') # Not in tiers
|
assert exchange.get_max_pair_stake_amount('TTT/USDT:USDT', 1.0) == float('inf') # Not in tiers
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('mode,side,reduceonly,result', [
|
@pytest.mark.parametrize('mode,side,reduceonly,result', [
|
||||||
|
@ -919,6 +919,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
|
|||||||
default_conf["trailing_stop_positive"] = data.trailing_stop_positive
|
default_conf["trailing_stop_positive"] = data.trailing_stop_positive
|
||||||
default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset
|
default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset
|
||||||
default_conf["use_exit_signal"] = data.use_exit_signal
|
default_conf["use_exit_signal"] = data.use_exit_signal
|
||||||
|
default_conf["max_open_trades"] = 10
|
||||||
|
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0)
|
mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0)
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
@ -951,7 +952,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
|
|||||||
processed=data_processed,
|
processed=data_processed,
|
||||||
start_date=min_date,
|
start_date=min_date,
|
||||||
end_date=max_date,
|
end_date=max_date,
|
||||||
max_open_trades=10,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
results = result['results']
|
results = result['results']
|
||||||
|
@ -19,12 +19,12 @@ from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi
|
|||||||
from freqtrade.data.converter import clean_ohlcv_dataframe
|
from freqtrade.data.converter import clean_ohlcv_dataframe
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.data.history import get_timerange
|
from freqtrade.data.history import get_timerange
|
||||||
from freqtrade.enums import ExitType, RunMode
|
from freqtrade.enums import CandleType, ExitType, RunMode
|
||||||
from freqtrade.exceptions import DependencyException, OperationalException
|
from freqtrade.exceptions import DependencyException, OperationalException
|
||||||
from freqtrade.exchange.exchange import timeframe_to_next_date
|
from freqtrade.exchange.exchange import timeframe_to_next_date
|
||||||
from freqtrade.optimize.backtest_caching import get_strategy_run_id
|
from freqtrade.optimize.backtest_caching import get_strategy_run_id
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
from freqtrade.persistence import LocalTrade
|
from freqtrade.persistence import LocalTrade, Trade
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange,
|
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange,
|
||||||
patched_configuration_load_config_file)
|
patched_configuration_load_config_file)
|
||||||
@ -96,7 +96,6 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
|
|||||||
'processed': processed,
|
'processed': processed,
|
||||||
'start_date': min_date,
|
'start_date': min_date,
|
||||||
'end_date': max_date,
|
'end_date': max_date,
|
||||||
'max_open_trades': 10,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -360,7 +359,6 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
|
|||||||
PropertyMock(return_value=['UNITTEST/BTC']))
|
PropertyMock(return_value=['UNITTEST/BTC']))
|
||||||
|
|
||||||
default_conf['timeframe'] = '1m'
|
default_conf['timeframe'] = '1m'
|
||||||
default_conf['datadir'] = testdatadir
|
|
||||||
default_conf['export'] = 'signals'
|
default_conf['export'] = 'signals'
|
||||||
default_conf['exportfilename'] = 'export.txt'
|
default_conf['exportfilename'] = 'export.txt'
|
||||||
default_conf['timerange'] = '-1510694220'
|
default_conf['timerange'] = '-1510694220'
|
||||||
@ -396,7 +394,6 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
|
|||||||
PropertyMock(return_value=['UNITTEST/BTC']))
|
PropertyMock(return_value=['UNITTEST/BTC']))
|
||||||
|
|
||||||
default_conf['timeframe'] = "1m"
|
default_conf['timeframe'] = "1m"
|
||||||
default_conf['datadir'] = testdatadir
|
|
||||||
default_conf['export'] = 'none'
|
default_conf['export'] = 'none'
|
||||||
default_conf['timerange'] = '20180101-20180102'
|
default_conf['timerange'] = '20180101-20180102'
|
||||||
|
|
||||||
@ -417,7 +414,6 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) ->
|
|||||||
PropertyMock(return_value=[]))
|
PropertyMock(return_value=[]))
|
||||||
|
|
||||||
default_conf['timeframe'] = "1m"
|
default_conf['timeframe'] = "1m"
|
||||||
default_conf['datadir'] = testdatadir
|
|
||||||
default_conf['export'] = 'none'
|
default_conf['export'] = 'none'
|
||||||
default_conf['timerange'] = '20180101-20180102'
|
default_conf['timerange'] = '20180101-20180102'
|
||||||
|
|
||||||
@ -451,7 +447,6 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti
|
|||||||
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.refresh_pairlist')
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.refresh_pairlist')
|
||||||
|
|
||||||
default_conf['ticker_interval'] = "1m"
|
default_conf['ticker_interval'] = "1m"
|
||||||
default_conf['datadir'] = testdatadir
|
|
||||||
default_conf['export'] = 'none'
|
default_conf['export'] = 'none'
|
||||||
# Use stoploss from strategy
|
# Use stoploss from strategy
|
||||||
del default_conf['stoploss']
|
del default_conf['stoploss']
|
||||||
@ -619,7 +614,7 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
|
|||||||
assert trade is None
|
assert trade is None
|
||||||
|
|
||||||
|
|
||||||
def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
|
def test_backtest__check_trade_exit(default_conf, fee, mocker) -> None:
|
||||||
default_conf['use_exit_signal'] = False
|
default_conf['use_exit_signal'] = False
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
@ -665,7 +660,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
|
|||||||
]
|
]
|
||||||
|
|
||||||
# No data available.
|
# No data available.
|
||||||
res = backtesting._get_exit_trade_entry(trade, row_sell, True)
|
res = backtesting._check_trade_exit(trade, row_sell)
|
||||||
assert res is not None
|
assert res is not None
|
||||||
assert res.exit_reason == ExitType.ROI.value
|
assert res.exit_reason == ExitType.ROI.value
|
||||||
assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc)
|
assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc)
|
||||||
@ -678,12 +673,14 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
|
|||||||
[], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
[], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
||||||
'enter_short', 'exit_short', 'long_tag', 'short_tag', 'exit_tag'])
|
'enter_short', 'exit_short', 'long_tag', 'short_tag', 'exit_tag'])
|
||||||
|
|
||||||
res = backtesting._get_exit_trade_entry(trade, row, True)
|
res = backtesting._check_trade_exit(trade, row)
|
||||||
assert res is None
|
assert res is None
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
||||||
default_conf['use_exit_signal'] = False
|
default_conf['use_exit_signal'] = False
|
||||||
|
default_conf['max_open_trades'] = 10
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||||
@ -701,7 +698,6 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
|||||||
processed=deepcopy(processed),
|
processed=deepcopy(processed),
|
||||||
start_date=min_date,
|
start_date=min_date,
|
||||||
end_date=max_date,
|
end_date=max_date,
|
||||||
max_open_trades=10,
|
|
||||||
)
|
)
|
||||||
results = result['results']
|
results = result['results']
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
@ -785,6 +781,8 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de
|
|||||||
def custom_entry_price(proposed_rate, **kwargs):
|
def custom_entry_price(proposed_rate, **kwargs):
|
||||||
return proposed_rate * 0.997
|
return proposed_rate * 0.997
|
||||||
|
|
||||||
|
default_conf_usdt['max_open_trades'] = 10
|
||||||
|
|
||||||
backtesting = Backtesting(default_conf_usdt)
|
backtesting = Backtesting(default_conf_usdt)
|
||||||
backtesting._set_strategy(backtesting.strategylist[0])
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
backtesting.strategy.populate_entry_trend = advise_entry
|
backtesting.strategy.populate_entry_trend = advise_entry
|
||||||
@ -792,10 +790,10 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de
|
|||||||
pair = 'XRP/ETH'
|
pair = 'XRP/ETH'
|
||||||
# Pick a timerange adapted to the pair we use to test
|
# Pick a timerange adapted to the pair we use to test
|
||||||
timerange = TimeRange.parse_timerange('20191010-20191013')
|
timerange = TimeRange.parse_timerange('20191010-20191013')
|
||||||
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['XRP/ETH'],
|
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=[pair],
|
||||||
timerange=timerange)
|
timerange=timerange)
|
||||||
if use_detail:
|
if use_detail:
|
||||||
data_1m = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['XRP/ETH'],
|
data_1m = history.load_data(datadir=testdatadir, timeframe='1m', pairs=[pair],
|
||||||
timerange=timerange)
|
timerange=timerange)
|
||||||
backtesting.detail_data = data_1m
|
backtesting.detail_data = data_1m
|
||||||
processed = backtesting.strategy.advise_all_indicators(data)
|
processed = backtesting.strategy.advise_all_indicators(data)
|
||||||
@ -805,7 +803,6 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de
|
|||||||
processed=deepcopy(processed),
|
processed=deepcopy(processed),
|
||||||
start_date=min_date,
|
start_date=min_date,
|
||||||
end_date=max_date,
|
end_date=max_date,
|
||||||
max_open_trades=10,
|
|
||||||
)
|
)
|
||||||
results = result['results']
|
results = result['results']
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
@ -849,6 +846,164 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de
|
|||||||
assert late_entry > 0
|
assert late_entry > 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('use_detail', [True, False])
|
||||||
|
def test_backtest_one_detail_futures(
|
||||||
|
default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None:
|
||||||
|
default_conf_usdt['use_exit_signal'] = False
|
||||||
|
default_conf_usdt['trading_mode'] = 'futures'
|
||||||
|
default_conf_usdt['margin_mode'] = 'isolated'
|
||||||
|
default_conf_usdt['candle_type_def'] = CandleType.FUTURES
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||||
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||||
|
PropertyMock(return_value=['XRP/USDT:USDT']))
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_maintenance_ratio_and_amt",
|
||||||
|
return_value=(0.01, 0.01))
|
||||||
|
default_conf_usdt['timeframe'] = '1h'
|
||||||
|
if use_detail:
|
||||||
|
default_conf_usdt['timeframe_detail'] = '5m'
|
||||||
|
patch_exchange(mocker)
|
||||||
|
|
||||||
|
def advise_entry(df, *args, **kwargs):
|
||||||
|
# Mock function to force several entries
|
||||||
|
df.loc[(df['rsi'] < 40), 'enter_long'] = 1
|
||||||
|
return df
|
||||||
|
|
||||||
|
def custom_entry_price(proposed_rate, **kwargs):
|
||||||
|
return proposed_rate * 0.997
|
||||||
|
|
||||||
|
default_conf_usdt['max_open_trades'] = 10
|
||||||
|
|
||||||
|
backtesting = Backtesting(default_conf_usdt)
|
||||||
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
|
backtesting.strategy.populate_entry_trend = advise_entry
|
||||||
|
backtesting.strategy.custom_entry_price = custom_entry_price
|
||||||
|
pair = 'XRP/USDT:USDT'
|
||||||
|
# Pick a timerange adapted to the pair we use to test
|
||||||
|
timerange = TimeRange.parse_timerange('20211117-20211119')
|
||||||
|
data = history.load_data(datadir=Path(testdatadir), timeframe='1h', pairs=[pair],
|
||||||
|
timerange=timerange, candle_type=CandleType.FUTURES)
|
||||||
|
backtesting.load_bt_data_detail()
|
||||||
|
processed = backtesting.strategy.advise_all_indicators(data)
|
||||||
|
min_date, max_date = get_timerange(processed)
|
||||||
|
|
||||||
|
result = backtesting.backtest(
|
||||||
|
processed=deepcopy(processed),
|
||||||
|
start_date=min_date,
|
||||||
|
end_date=max_date,
|
||||||
|
)
|
||||||
|
results = result['results']
|
||||||
|
assert not results.empty
|
||||||
|
# Timeout settings from default_conf = entry: 10, exit: 30
|
||||||
|
assert len(results) == (5 if use_detail else 2)
|
||||||
|
|
||||||
|
assert 'orders' in results.columns
|
||||||
|
data_pair = processed[pair]
|
||||||
|
|
||||||
|
data_1m_pair = backtesting.detail_data[pair] if use_detail else pd.DataFrame()
|
||||||
|
late_entry = 0
|
||||||
|
for _, t in results.iterrows():
|
||||||
|
assert len(t['orders']) == 2
|
||||||
|
|
||||||
|
entryo = t['orders'][0]
|
||||||
|
entry_ts = datetime.fromtimestamp(entryo['order_filled_timestamp'] // 1000, tz=timezone.utc)
|
||||||
|
if entry_ts > t['open_date']:
|
||||||
|
late_entry += 1
|
||||||
|
|
||||||
|
# Get "entry fill" candle
|
||||||
|
ln = (data_1m_pair.loc[data_1m_pair["date"] == entry_ts]
|
||||||
|
if use_detail else data_pair.loc[data_pair["date"] == entry_ts])
|
||||||
|
# Check open trade rate aligns to open rate
|
||||||
|
assert not ln.empty
|
||||||
|
|
||||||
|
assert round(ln.iloc[0]["low"], 6) <= round(
|
||||||
|
t["open_rate"], 6) <= round(ln.iloc[0]["high"], 6)
|
||||||
|
# check close trade rate aligns to close rate or is between high and low
|
||||||
|
ln1 = data_pair.loc[data_pair["date"] == t["close_date"]]
|
||||||
|
if use_detail:
|
||||||
|
ln1_1m = data_1m_pair.loc[data_1m_pair["date"] == t["close_date"]]
|
||||||
|
assert not ln1.empty or not ln1_1m.empty
|
||||||
|
else:
|
||||||
|
assert not ln1.empty
|
||||||
|
ln2 = ln1_1m if ln1.empty else ln1
|
||||||
|
|
||||||
|
assert (round(ln2.iloc[0]["low"], 6) <= round(
|
||||||
|
t["close_rate"], 6) <= round(ln2.iloc[0]["high"], 6))
|
||||||
|
assert -0.0181 < Trade.trades[1].funding_fees < -0.01
|
||||||
|
# assert late_entry > 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('use_detail', [True, False])
|
||||||
|
def test_backtest_one_detail_futures_funding_fees(
|
||||||
|
default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None:
|
||||||
|
default_conf_usdt['use_exit_signal'] = False
|
||||||
|
default_conf_usdt['trading_mode'] = 'futures'
|
||||||
|
default_conf_usdt['margin_mode'] = 'isolated'
|
||||||
|
default_conf_usdt['candle_type_def'] = CandleType.FUTURES
|
||||||
|
default_conf_usdt['minimal_roi'] = {'0': 1}
|
||||||
|
default_conf_usdt['dry_run_wallet'] = 100000
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||||
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||||
|
PropertyMock(return_value=['XRP/USDT:USDT']))
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_maintenance_ratio_and_amt",
|
||||||
|
return_value=(0.01, 0.01))
|
||||||
|
default_conf_usdt['timeframe'] = '1h'
|
||||||
|
if use_detail:
|
||||||
|
default_conf_usdt['timeframe_detail'] = '5m'
|
||||||
|
patch_exchange(mocker)
|
||||||
|
|
||||||
|
def advise_entry(df, *args, **kwargs):
|
||||||
|
# Mock function to force several entries
|
||||||
|
df.loc[:, 'enter_long'] = 1
|
||||||
|
return df
|
||||||
|
|
||||||
|
def adjust_trade_position(trade, current_time, **kwargs):
|
||||||
|
if current_time > datetime(2021, 11, 18, 2, 0, 0, tzinfo=timezone.utc):
|
||||||
|
return None
|
||||||
|
return default_conf_usdt['stake_amount']
|
||||||
|
|
||||||
|
default_conf_usdt['max_open_trades'] = 1
|
||||||
|
|
||||||
|
backtesting = Backtesting(default_conf_usdt)
|
||||||
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
|
backtesting.strategy.populate_entry_trend = advise_entry
|
||||||
|
backtesting.strategy.adjust_trade_position = adjust_trade_position
|
||||||
|
backtesting.strategy.leverage = lambda **kwargs: 1
|
||||||
|
backtesting.strategy.position_adjustment_enable = True
|
||||||
|
pair = 'XRP/USDT:USDT'
|
||||||
|
# Pick a timerange adapted to the pair we use to test
|
||||||
|
timerange = TimeRange.parse_timerange('20211117-20211119')
|
||||||
|
data = history.load_data(datadir=Path(testdatadir), timeframe='1h', pairs=[pair],
|
||||||
|
timerange=timerange, candle_type=CandleType.FUTURES)
|
||||||
|
backtesting.load_bt_data_detail()
|
||||||
|
processed = backtesting.strategy.advise_all_indicators(data)
|
||||||
|
min_date, max_date = get_timerange(processed)
|
||||||
|
|
||||||
|
result = backtesting.backtest(
|
||||||
|
processed=deepcopy(processed),
|
||||||
|
start_date=min_date,
|
||||||
|
end_date=max_date,
|
||||||
|
)
|
||||||
|
results = result['results']
|
||||||
|
assert not results.empty
|
||||||
|
# Only one result - as we're not selling.
|
||||||
|
assert len(results) == 1
|
||||||
|
|
||||||
|
assert 'orders' in results.columns
|
||||||
|
|
||||||
|
for t in Trade.trades:
|
||||||
|
# At least 4 adjustment orders
|
||||||
|
assert t.nr_of_successful_entries >= 6
|
||||||
|
# Funding fees will vary depending on the number of adjustment orders
|
||||||
|
# That number is a lot higher with detail data.
|
||||||
|
assert -20 < t.funding_fees < -0.1
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir) -> None:
|
def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir) -> None:
|
||||||
# This strategy intentionally places unfillable orders.
|
# This strategy intentionally places unfillable orders.
|
||||||
default_conf['strategy'] = 'StrategyTestV3CustomEntryPrice'
|
default_conf['strategy'] = 'StrategyTestV3CustomEntryPrice'
|
||||||
@ -859,6 +1014,7 @@ def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir)
|
|||||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
default_conf['max_open_trades'] = 1
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting._set_strategy(backtesting.strategylist[0])
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
# Testing dataframe contains 11 candles. Expecting 10 timed out orders.
|
# Testing dataframe contains 11 candles. Expecting 10 timed out orders.
|
||||||
@ -871,7 +1027,6 @@ def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir)
|
|||||||
processed=deepcopy(data),
|
processed=deepcopy(data),
|
||||||
start_date=min_date,
|
start_date=min_date,
|
||||||
end_date=max_date,
|
end_date=max_date,
|
||||||
max_open_trades=1,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result['timedout_entry_orders'] == 10
|
assert result['timedout_entry_orders'] == 10
|
||||||
@ -879,6 +1034,7 @@ def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir)
|
|||||||
|
|
||||||
def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None:
|
def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None:
|
||||||
default_conf['use_exit_signal'] = False
|
default_conf['use_exit_signal'] = False
|
||||||
|
default_conf['max_open_trades'] = 1
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||||
@ -896,7 +1052,6 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
|
|||||||
processed=processed,
|
processed=processed,
|
||||||
start_date=min_date,
|
start_date=min_date,
|
||||||
end_date=max_date,
|
end_date=max_date,
|
||||||
max_open_trades=1,
|
|
||||||
)
|
)
|
||||||
assert not results['results'].empty
|
assert not results['results'].empty
|
||||||
assert len(results['results']) == 1
|
assert len(results['results']) == 1
|
||||||
@ -904,6 +1059,8 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
|
|||||||
|
|
||||||
def test_backtest_trim_no_data_left(default_conf, fee, mocker, testdatadir) -> None:
|
def test_backtest_trim_no_data_left(default_conf, fee, mocker, testdatadir) -> None:
|
||||||
default_conf['use_exit_signal'] = False
|
default_conf['use_exit_signal'] = False
|
||||||
|
default_conf['max_open_trades'] = 10
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||||
@ -927,7 +1084,6 @@ def test_backtest_trim_no_data_left(default_conf, fee, mocker, testdatadir) -> N
|
|||||||
processed=deepcopy(processed),
|
processed=deepcopy(processed),
|
||||||
start_date=min_date,
|
start_date=min_date,
|
||||||
end_date=max_date,
|
end_date=max_date,
|
||||||
max_open_trades=10,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -948,6 +1104,7 @@ def test_processed(default_conf, mocker, testdatadir) -> None:
|
|||||||
|
|
||||||
def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadir) -> None:
|
def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadir) -> None:
|
||||||
default_conf['use_exit_signal'] = False
|
default_conf['use_exit_signal'] = False
|
||||||
|
default_conf['max_open_trades'] = 10
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=100000)
|
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=100000)
|
||||||
@ -981,7 +1138,6 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi
|
|||||||
processed=deepcopy(processed),
|
processed=deepcopy(processed),
|
||||||
start_date=min_date,
|
start_date=min_date,
|
||||||
end_date=max_date,
|
end_date=max_date,
|
||||||
max_open_trades=10,
|
|
||||||
)
|
)
|
||||||
assert count == 5
|
assert count == 5
|
||||||
|
|
||||||
@ -998,6 +1154,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
|
|||||||
|
|
||||||
default_conf['enable_protections'] = True
|
default_conf['enable_protections'] = True
|
||||||
default_conf['timeframe'] = '1m'
|
default_conf['timeframe'] = '1m'
|
||||||
|
default_conf['max_open_trades'] = 1
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||||
@ -1024,7 +1181,6 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
|
|||||||
processed=processed,
|
processed=processed,
|
||||||
start_date=min_date,
|
start_date=min_date,
|
||||||
end_date=max_date,
|
end_date=max_date,
|
||||||
max_open_trades=1,
|
|
||||||
)
|
)
|
||||||
assert len(results['results']) == numres
|
assert len(results['results']) == numres
|
||||||
|
|
||||||
@ -1062,11 +1218,12 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir,
|
|||||||
processed = backtesting.strategy.advise_all_indicators(data)
|
processed = backtesting.strategy.advise_all_indicators(data)
|
||||||
min_date, max_date = get_timerange(processed)
|
min_date, max_date = get_timerange(processed)
|
||||||
assert isinstance(processed, dict)
|
assert isinstance(processed, dict)
|
||||||
|
backtesting.strategy.max_open_trades = 1
|
||||||
|
backtesting.config.update({'max_open_trades': 1})
|
||||||
results = backtesting.backtest(
|
results = backtesting.backtest(
|
||||||
processed=processed,
|
processed=processed,
|
||||||
start_date=min_date,
|
start_date=min_date,
|
||||||
end_date=max_date,
|
end_date=max_date,
|
||||||
max_open_trades=1,
|
|
||||||
)
|
)
|
||||||
assert len(results['results']) == expected
|
assert len(results['results']) == expected
|
||||||
|
|
||||||
@ -1077,7 +1234,7 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):
|
|||||||
buy_value = 1
|
buy_value = 1
|
||||||
sell_value = 1
|
sell_value = 1
|
||||||
return _trend(dataframe, buy_value, sell_value)
|
return _trend(dataframe, buy_value, sell_value)
|
||||||
|
default_conf['max_open_trades'] = 10
|
||||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting._set_strategy(backtesting.strategylist[0])
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
@ -1094,6 +1251,7 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir):
|
|||||||
sell_value = 1
|
sell_value = 1
|
||||||
return _trend(dataframe, buy_value, sell_value)
|
return _trend(dataframe, buy_value, sell_value)
|
||||||
|
|
||||||
|
default_conf['max_open_trades'] = 10
|
||||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting._set_strategy(backtesting.strategylist[0])
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
@ -1107,6 +1265,7 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
|
|||||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
default_conf['max_open_trades'] = 10
|
||||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf,
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf,
|
||||||
pair='UNITTEST/BTC', datadir=testdatadir)
|
pair='UNITTEST/BTC', datadir=testdatadir)
|
||||||
default_conf['timeframe'] = '1m'
|
default_conf['timeframe'] = '1m'
|
||||||
@ -1165,6 +1324,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
|||||||
if tres > 0:
|
if tres > 0:
|
||||||
data[pair] = data[pair][tres:].reset_index()
|
data[pair] = data[pair][tres:].reset_index()
|
||||||
default_conf['timeframe'] = '5m'
|
default_conf['timeframe'] = '5m'
|
||||||
|
default_conf['max_open_trades'] = 3
|
||||||
|
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting._set_strategy(backtesting.strategylist[0])
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
@ -1173,11 +1333,11 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
|||||||
|
|
||||||
processed = backtesting.strategy.advise_all_indicators(data)
|
processed = backtesting.strategy.advise_all_indicators(data)
|
||||||
min_date, max_date = get_timerange(processed)
|
min_date, max_date = get_timerange(processed)
|
||||||
|
|
||||||
backtest_conf = {
|
backtest_conf = {
|
||||||
'processed': deepcopy(processed),
|
'processed': deepcopy(processed),
|
||||||
'start_date': min_date,
|
'start_date': min_date,
|
||||||
'end_date': max_date,
|
'end_date': max_date,
|
||||||
'max_open_trades': 3,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
results = backtesting.backtest(**backtest_conf)
|
results = backtesting.backtest(**backtest_conf)
|
||||||
@ -1195,11 +1355,12 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
|||||||
backtesting.dataprovider.get_analyzed_dataframe('NXT/BTC', '5m')[0]
|
backtesting.dataprovider.get_analyzed_dataframe('NXT/BTC', '5m')[0]
|
||||||
) == len(data['NXT/BTC']) - 1 - backtesting.strategy.startup_candle_count
|
) == len(data['NXT/BTC']) - 1 - backtesting.strategy.startup_candle_count
|
||||||
|
|
||||||
|
backtesting.strategy.max_open_trades = 1
|
||||||
|
backtesting.config.update({'max_open_trades': 1})
|
||||||
backtest_conf = {
|
backtest_conf = {
|
||||||
'processed': deepcopy(processed),
|
'processed': deepcopy(processed),
|
||||||
'start_date': min_date,
|
'start_date': min_date,
|
||||||
'end_date': max_date,
|
'end_date': max_date,
|
||||||
'max_open_trades': 1,
|
|
||||||
}
|
}
|
||||||
results = backtesting.backtest(**backtest_conf)
|
results = backtesting.backtest(**backtest_conf)
|
||||||
assert len(evaluate_result_multi(results['results'], '5m', 1)) == 0
|
assert len(evaluate_result_multi(results['results'], '5m', 1)) == 0
|
||||||
@ -1460,7 +1621,7 @@ def test_backtest_start_futures_noliq(default_conf_usdt, mocker,
|
|||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
|
||||||
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||||
PropertyMock(return_value=['HULUMULU/USDT', 'XRP/USDT']))
|
PropertyMock(return_value=['HULUMULU/USDT', 'XRP/USDT:USDT']))
|
||||||
# mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
# mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
||||||
|
|
||||||
patched_configuration_load_config_file(mocker, default_conf_usdt)
|
patched_configuration_load_config_file(mocker, default_conf_usdt)
|
||||||
@ -1491,7 +1652,7 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
|
|||||||
"strategy": CURRENT_TEST_STRATEGY,
|
"strategy": CURRENT_TEST_STRATEGY,
|
||||||
})
|
})
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
result1 = pd.DataFrame({'pair': ['XRP/USDT', 'XRP/USDT'],
|
result1 = pd.DataFrame({'pair': ['XRP/USDT:USDT', 'XRP/USDT:USDT'],
|
||||||
'profit_ratio': [0.0, 0.0],
|
'profit_ratio': [0.0, 0.0],
|
||||||
'profit_abs': [0.0, 0.0],
|
'profit_abs': [0.0, 0.0],
|
||||||
'open_date': pd.to_datetime(['2021-11-18 18:00:00',
|
'open_date': pd.to_datetime(['2021-11-18 18:00:00',
|
||||||
@ -1507,7 +1668,7 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
|
|||||||
'close_rate': [0.104969, 0.103541],
|
'close_rate': [0.104969, 0.103541],
|
||||||
'exit_reason': [ExitType.ROI, ExitType.ROI]
|
'exit_reason': [ExitType.ROI, ExitType.ROI]
|
||||||
})
|
})
|
||||||
result2 = pd.DataFrame({'pair': ['XRP/USDT', 'XRP/USDT', 'XRP/USDT'],
|
result2 = pd.DataFrame({'pair': ['XRP/USDT:USDT', 'XRP/USDT:USDT', 'XRP/USDT:USDT'],
|
||||||
'profit_ratio': [0.03, 0.01, 0.1],
|
'profit_ratio': [0.03, 0.01, 0.1],
|
||||||
'profit_abs': [0.01, 0.02, 0.2],
|
'profit_abs': [0.01, 0.02, 0.2],
|
||||||
'open_date': pd.to_datetime(['2021-11-19 18:00:00',
|
'open_date': pd.to_datetime(['2021-11-19 18:00:00',
|
||||||
@ -1552,7 +1713,7 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||||
PropertyMock(return_value=['XRP/USDT']))
|
PropertyMock(return_value=['XRP/USDT:USDT']))
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
||||||
|
|
||||||
patched_configuration_load_config_file(mocker, default_conf_usdt)
|
patched_configuration_load_config_file(mocker, default_conf_usdt)
|
||||||
@ -1575,8 +1736,8 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
|
|||||||
'up to 2021-11-21 04:00:00 (4 days).',
|
'up to 2021-11-21 04:00:00 (4 days).',
|
||||||
'Backtesting with data from 2021-11-17 21:00:00 '
|
'Backtesting with data from 2021-11-17 21:00:00 '
|
||||||
'up to 2021-11-21 04:00:00 (3 days).',
|
'up to 2021-11-21 04:00:00 (3 days).',
|
||||||
'XRP/USDT, funding_rate, 8h, data starts at 2021-11-18 00:00:00',
|
'XRP/USDT:USDT, funding_rate, 8h, data starts at 2021-11-18 00:00:00',
|
||||||
'XRP/USDT, mark, 8h, data starts at 2021-11-18 00:00:00',
|
'XRP/USDT:USDT, mark, 8h, data starts at 2021-11-18 00:00:00',
|
||||||
f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}',
|
f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ from tests.conftest import patch_exchange
|
|||||||
|
|
||||||
def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> None:
|
def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> None:
|
||||||
default_conf['use_exit_signal'] = False
|
default_conf['use_exit_signal'] = False
|
||||||
|
default_conf['max_open_trades'] = 10
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
mocker.patch('freqtrade.optimize.backtesting.amount_to_contract_precision',
|
mocker.patch('freqtrade.optimize.backtesting.amount_to_contract_precision',
|
||||||
lambda x, *args, **kwargs: round(x, 8))
|
lambda x, *args, **kwargs: round(x, 8))
|
||||||
@ -41,7 +42,6 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
|
|||||||
processed=deepcopy(processed),
|
processed=deepcopy(processed),
|
||||||
start_date=min_date,
|
start_date=min_date,
|
||||||
end_date=max_date,
|
end_date=max_date,
|
||||||
max_open_trades=10,
|
|
||||||
)
|
)
|
||||||
results = result['results']
|
results = result['results']
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212,C0103
|
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from functools import wraps
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import ANY, MagicMock, PropertyMock
|
from unittest.mock import ANY, MagicMock, PropertyMock
|
||||||
|
|
||||||
@ -7,6 +8,7 @@ import pandas as pd
|
|||||||
import pytest
|
import pytest
|
||||||
from arrow import Arrow
|
from arrow import Arrow
|
||||||
from filelock import Timeout
|
from filelock import Timeout
|
||||||
|
from skopt.space import Integer
|
||||||
|
|
||||||
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt
|
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt
|
||||||
from freqtrade.data.history import load_data
|
from freqtrade.data.history import load_data
|
||||||
@ -292,6 +294,8 @@ def test_params_no_optimize_details(hyperopt) -> None:
|
|||||||
assert res['roi']['0'] == 0.04
|
assert res['roi']['0'] == 0.04
|
||||||
assert "stoploss" in res
|
assert "stoploss" in res
|
||||||
assert res['stoploss']['stoploss'] == -0.1
|
assert res['stoploss']['stoploss'] == -0.1
|
||||||
|
assert "max_open_trades" in res
|
||||||
|
assert res['max_open_trades']['max_open_trades'] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
|
def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
|
||||||
@ -334,8 +338,7 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
|
|||||||
assert dumper2.call_count == 1
|
assert dumper2.call_count == 1
|
||||||
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
|
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
|
||||||
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
|
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
|
||||||
assert hasattr(hyperopt, "max_open_trades")
|
assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades']
|
||||||
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
|
|
||||||
assert hasattr(hyperopt.backtesting, "_position_stacking")
|
assert hasattr(hyperopt.backtesting, "_position_stacking")
|
||||||
|
|
||||||
|
|
||||||
@ -474,6 +477,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
|||||||
'trailing_stop_positive': 0.02,
|
'trailing_stop_positive': 0.02,
|
||||||
'trailing_stop_positive_offset_p1': 0.05,
|
'trailing_stop_positive_offset_p1': 0.05,
|
||||||
'trailing_only_offset_is_reached': False,
|
'trailing_only_offset_is_reached': False,
|
||||||
|
'max_open_trades': 3,
|
||||||
}
|
}
|
||||||
response_expected = {
|
response_expected = {
|
||||||
'loss': 1.9147239021396234,
|
'loss': 1.9147239021396234,
|
||||||
@ -499,7 +503,9 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
|||||||
'trailing': {'trailing_only_offset_is_reached': False,
|
'trailing': {'trailing_only_offset_is_reached': False,
|
||||||
'trailing_stop': True,
|
'trailing_stop': True,
|
||||||
'trailing_stop_positive': 0.02,
|
'trailing_stop_positive': 0.02,
|
||||||
'trailing_stop_positive_offset': 0.07}},
|
'trailing_stop_positive_offset': 0.07},
|
||||||
|
'max_open_trades': {'max_open_trades': 3}
|
||||||
|
},
|
||||||
'params_dict': optimizer_param,
|
'params_dict': optimizer_param,
|
||||||
'params_not_optimized': {'buy': {}, 'protection': {}, 'sell': {}},
|
'params_not_optimized': {'buy': {}, 'protection': {}, 'sell': {}},
|
||||||
'results_metrics': ANY,
|
'results_metrics': ANY,
|
||||||
@ -548,7 +554,8 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
|
|||||||
'buy': {'mfi-value': None},
|
'buy': {'mfi-value': None},
|
||||||
'sell': {'sell-mfi-value': None},
|
'sell': {'sell-mfi-value': None},
|
||||||
'roi': {}, 'stoploss': {'stoploss': None},
|
'roi': {}, 'stoploss': {'stoploss': None},
|
||||||
'trailing': {'trailing_stop': None}
|
'trailing': {'trailing_stop': None},
|
||||||
|
'max_open_trades': {'max_open_trades': None}
|
||||||
},
|
},
|
||||||
'results_metrics': generate_result_metrics(),
|
'results_metrics': generate_result_metrics(),
|
||||||
}])
|
}])
|
||||||
@ -571,7 +578,7 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
|
|||||||
out, err = capsys.readouterr()
|
out, err = capsys.readouterr()
|
||||||
result_str = (
|
result_str = (
|
||||||
'{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi"'
|
'{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi"'
|
||||||
':{},"stoploss":null,"trailing_stop":null}'
|
':{},"stoploss":null,"trailing_stop":null,"max_open_trades":null}'
|
||||||
)
|
)
|
||||||
assert result_str in out # noqa: E501
|
assert result_str in out # noqa: E501
|
||||||
# Should be called for historical candle data
|
# Should be called for historical candle data
|
||||||
@ -702,8 +709,7 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non
|
|||||||
|
|
||||||
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
|
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
|
||||||
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
|
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
|
||||||
assert hasattr(hyperopt, "max_open_trades")
|
assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades']
|
||||||
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
|
|
||||||
assert hasattr(hyperopt.backtesting, "_position_stacking")
|
assert hasattr(hyperopt.backtesting, "_position_stacking")
|
||||||
|
|
||||||
|
|
||||||
@ -776,8 +782,7 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
|
|||||||
assert dumper2.call_count == 1
|
assert dumper2.call_count == 1
|
||||||
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
|
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
|
||||||
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
|
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
|
||||||
assert hasattr(hyperopt, "max_open_trades")
|
assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades']
|
||||||
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
|
|
||||||
assert hasattr(hyperopt.backtesting, "_position_stacking")
|
assert hasattr(hyperopt.backtesting, "_position_stacking")
|
||||||
|
|
||||||
|
|
||||||
@ -819,8 +824,7 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
|
|||||||
assert dumper2.call_count == 1
|
assert dumper2.call_count == 1
|
||||||
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
|
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
|
||||||
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
|
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
|
||||||
assert hasattr(hyperopt, "max_open_trades")
|
assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades']
|
||||||
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
|
|
||||||
assert hasattr(hyperopt.backtesting, "_position_stacking")
|
assert hasattr(hyperopt.backtesting, "_position_stacking")
|
||||||
|
|
||||||
|
|
||||||
@ -874,6 +878,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
|||||||
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
|
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
|
||||||
assert hyperopt.backtesting.strategy.sell_rsi.value == 74
|
assert hyperopt.backtesting.strategy.sell_rsi.value == 74
|
||||||
assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30
|
assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30
|
||||||
|
assert hyperopt.backtesting.strategy.max_open_trades == 1
|
||||||
buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range
|
buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range
|
||||||
assert isinstance(buy_rsi_range, range)
|
assert isinstance(buy_rsi_range, range)
|
||||||
# Range from 0 - 50 (inclusive)
|
# Range from 0 - 50 (inclusive)
|
||||||
@ -884,6 +889,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
|||||||
assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value != 30
|
assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value != 30
|
||||||
assert hyperopt.backtesting.strategy.buy_rsi.value != 35
|
assert hyperopt.backtesting.strategy.buy_rsi.value != 35
|
||||||
assert hyperopt.backtesting.strategy.sell_rsi.value != 74
|
assert hyperopt.backtesting.strategy.sell_rsi.value != 74
|
||||||
|
assert hyperopt.backtesting.strategy.max_open_trades != 1
|
||||||
|
|
||||||
hyperopt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: 'ET1'
|
hyperopt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: 'ET1'
|
||||||
with pytest.raises(OperationalException, match="Estimator ET1 not supported."):
|
with pytest.raises(OperationalException, match="Estimator ET1 not supported."):
|
||||||
@ -984,3 +990,124 @@ def test_SKDecimal():
|
|||||||
assert space.transform([2.0]) == [200]
|
assert space.transform([2.0]) == [200]
|
||||||
assert space.transform([1.0]) == [100]
|
assert space.transform([1.0]) == [100]
|
||||||
assert space.transform([1.5, 1.6]) == [150, 160]
|
assert space.transform([1.5, 1.6]) == [150, 160]
|
||||||
|
|
||||||
|
|
||||||
|
def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
||||||
|
# This test is to ensure that unlimited max_open_trades are ignored for the backtesting
|
||||||
|
# if we have an unlimited stake amount
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
|
||||||
|
hyperopt_conf.update({
|
||||||
|
'strategy': 'HyperoptableStrategy',
|
||||||
|
'user_data_dir': Path(tmpdir),
|
||||||
|
'hyperopt_random_state': 42,
|
||||||
|
'spaces': ['trades'],
|
||||||
|
'stake_amount': 'unlimited'
|
||||||
|
})
|
||||||
|
hyperopt = Hyperopt(hyperopt_conf)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._get_params_dict',
|
||||||
|
return_value={
|
||||||
|
'max_open_trades': -1
|
||||||
|
})
|
||||||
|
|
||||||
|
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
||||||
|
|
||||||
|
assert hyperopt.backtesting.strategy.max_open_trades == 1
|
||||||
|
|
||||||
|
hyperopt.start()
|
||||||
|
|
||||||
|
assert hyperopt.backtesting.strategy.max_open_trades == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_max_open_trades_dump(mocker, hyperopt_conf, tmpdir, fee, capsys) -> None:
|
||||||
|
# This test is to ensure that after hyperopting, max_open_trades is never
|
||||||
|
# saved as inf in the output json params
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
|
||||||
|
hyperopt_conf.update({
|
||||||
|
'strategy': 'HyperoptableStrategy',
|
||||||
|
'user_data_dir': Path(tmpdir),
|
||||||
|
'hyperopt_random_state': 42,
|
||||||
|
'spaces': ['trades'],
|
||||||
|
})
|
||||||
|
hyperopt = Hyperopt(hyperopt_conf)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._get_params_dict',
|
||||||
|
return_value={
|
||||||
|
'max_open_trades': -1
|
||||||
|
})
|
||||||
|
|
||||||
|
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
||||||
|
|
||||||
|
hyperopt.start()
|
||||||
|
|
||||||
|
out, err = capsys.readouterr()
|
||||||
|
|
||||||
|
assert 'max_open_trades = -1' in out
|
||||||
|
assert 'max_open_trades = inf' not in out
|
||||||
|
|
||||||
|
##############
|
||||||
|
|
||||||
|
hyperopt_conf.update({'print_json': True})
|
||||||
|
|
||||||
|
hyperopt = Hyperopt(hyperopt_conf)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._get_params_dict',
|
||||||
|
return_value={
|
||||||
|
'max_open_trades': -1
|
||||||
|
})
|
||||||
|
|
||||||
|
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
||||||
|
|
||||||
|
hyperopt.start()
|
||||||
|
|
||||||
|
out, err = capsys.readouterr()
|
||||||
|
|
||||||
|
assert '"max_open_trades":-1' in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_max_open_trades_consistency(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
||||||
|
# This test is to ensure that max_open_trades is the same across all functions needing it
|
||||||
|
# after it has been changed from the hyperopt
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0)
|
||||||
|
|
||||||
|
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
|
||||||
|
hyperopt_conf.update({
|
||||||
|
'strategy': 'HyperoptableStrategy',
|
||||||
|
'user_data_dir': Path(tmpdir),
|
||||||
|
'hyperopt_random_state': 42,
|
||||||
|
'spaces': ['trades'],
|
||||||
|
'stake_amount': 'unlimited',
|
||||||
|
'dry_run_wallet': 8,
|
||||||
|
'available_capital': 8,
|
||||||
|
'dry_run': True,
|
||||||
|
'epochs': 1
|
||||||
|
})
|
||||||
|
hyperopt = Hyperopt(hyperopt_conf)
|
||||||
|
|
||||||
|
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
||||||
|
|
||||||
|
hyperopt.custom_hyperopt.max_open_trades_space = lambda: [
|
||||||
|
Integer(1, 10, name='max_open_trades')]
|
||||||
|
|
||||||
|
first_time_evaluated = False
|
||||||
|
|
||||||
|
def stake_amount_interceptor(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
nonlocal first_time_evaluated
|
||||||
|
stake_amount = func(*args, **kwargs)
|
||||||
|
if first_time_evaluated is False:
|
||||||
|
assert stake_amount == 1
|
||||||
|
first_time_evaluated = True
|
||||||
|
return stake_amount
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
hyperopt.backtesting.wallets._calculate_unlimited_stake_amount = stake_amount_interceptor(
|
||||||
|
hyperopt.backtesting.wallets._calculate_unlimited_stake_amount)
|
||||||
|
|
||||||
|
hyperopt.start()
|
||||||
|
|
||||||
|
assert hyperopt.backtesting.strategy.max_open_trades == 8
|
||||||
|
assert hyperopt.config['max_open_trades'] == 8
|
||||||
|
@ -66,52 +66,58 @@ def test_load_previous_results2(mocker, testdatadir, caplog) -> None:
|
|||||||
@pytest.mark.parametrize("spaces, expected_results", [
|
@pytest.mark.parametrize("spaces, expected_results", [
|
||||||
(['buy'],
|
(['buy'],
|
||||||
{'buy': True, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
|
{'buy': True, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
|
||||||
'protection': False}),
|
'protection': False, 'trades': False}),
|
||||||
(['sell'],
|
(['sell'],
|
||||||
{'buy': False, 'sell': True, 'roi': False, 'stoploss': False, 'trailing': False,
|
{'buy': False, 'sell': True, 'roi': False, 'stoploss': False, 'trailing': False,
|
||||||
'protection': False}),
|
'protection': False, 'trades': False}),
|
||||||
(['roi'],
|
(['roi'],
|
||||||
{'buy': False, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False,
|
{'buy': False, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False,
|
||||||
'protection': False}),
|
'protection': False, 'trades': False}),
|
||||||
(['stoploss'],
|
(['stoploss'],
|
||||||
{'buy': False, 'sell': False, 'roi': False, 'stoploss': True, 'trailing': False,
|
{'buy': False, 'sell': False, 'roi': False, 'stoploss': True, 'trailing': False,
|
||||||
'protection': False}),
|
'protection': False, 'trades': False}),
|
||||||
(['trailing'],
|
(['trailing'],
|
||||||
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': True,
|
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': True,
|
||||||
'protection': False}),
|
'protection': False, 'trades': False}),
|
||||||
(['buy', 'sell', 'roi', 'stoploss'],
|
(['buy', 'sell', 'roi', 'stoploss'],
|
||||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
|
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
|
||||||
'protection': False}),
|
'protection': False, 'trades': False}),
|
||||||
(['buy', 'sell', 'roi', 'stoploss', 'trailing'],
|
(['buy', 'sell', 'roi', 'stoploss', 'trailing'],
|
||||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
||||||
'protection': False}),
|
'protection': False, 'trades': False}),
|
||||||
(['buy', 'roi'],
|
(['buy', 'roi'],
|
||||||
{'buy': True, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False,
|
{'buy': True, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False,
|
||||||
'protection': False}),
|
'protection': False, 'trades': False}),
|
||||||
(['all'],
|
(['all'],
|
||||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
||||||
'protection': True}),
|
'protection': True, 'trades': True}),
|
||||||
(['default'],
|
(['default'],
|
||||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
|
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
|
||||||
'protection': False}),
|
'protection': False, 'trades': False}),
|
||||||
(['default', 'trailing'],
|
(['default', 'trailing'],
|
||||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
||||||
'protection': False}),
|
'protection': False, 'trades': False}),
|
||||||
(['all', 'buy'],
|
(['all', 'buy'],
|
||||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
||||||
'protection': True}),
|
'protection': True, 'trades': True}),
|
||||||
(['default', 'buy'],
|
(['default', 'buy'],
|
||||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
|
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
|
||||||
'protection': False}),
|
'protection': False, 'trades': False}),
|
||||||
(['all'],
|
(['all'],
|
||||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
||||||
'protection': True}),
|
'protection': True, 'trades': True}),
|
||||||
(['protection'],
|
(['protection'],
|
||||||
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
|
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
|
||||||
'protection': True}),
|
'protection': True, 'trades': False}),
|
||||||
|
(['trades'],
|
||||||
|
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
|
||||||
|
'protection': False, 'trades': True}),
|
||||||
|
(['default', 'trades'],
|
||||||
|
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
|
||||||
|
'protection': False, 'trades': True}),
|
||||||
])
|
])
|
||||||
def test_has_space(hyperopt_conf, spaces, expected_results):
|
def test_has_space(hyperopt_conf, spaces, expected_results):
|
||||||
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection']:
|
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'trades']:
|
||||||
hyperopt_conf.update({'spaces': spaces})
|
hyperopt_conf.update({'spaces': spaces})
|
||||||
assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s]
|
assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s]
|
||||||
|
|
||||||
@ -193,6 +199,9 @@ def test_export_params(tmpdir):
|
|||||||
"346": 0.08499,
|
"346": 0.08499,
|
||||||
"507": 0.049,
|
"507": 0.049,
|
||||||
"1595": 0
|
"1595": 0
|
||||||
|
},
|
||||||
|
"max_open_trades": {
|
||||||
|
"max_open_trades": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"params_not_optimized": {
|
"params_not_optimized": {
|
||||||
@ -219,6 +228,7 @@ def test_export_params(tmpdir):
|
|||||||
assert "roi" in content["params"]
|
assert "roi" in content["params"]
|
||||||
assert "stoploss" in content["params"]
|
assert "stoploss" in content["params"]
|
||||||
assert "trailing" in content["params"]
|
assert "trailing" in content["params"]
|
||||||
|
assert "max_open_trades" in content["params"]
|
||||||
|
|
||||||
|
|
||||||
def test_try_export_params(default_conf, tmpdir, caplog, mocker):
|
def test_try_export_params(default_conf, tmpdir, caplog, mocker):
|
||||||
@ -297,6 +307,9 @@ def test_params_print(capsys):
|
|||||||
"trailing_stop_positive_offset": 0.1,
|
"trailing_stop_positive_offset": 0.1,
|
||||||
"trailing_only_offset_is_reached": True
|
"trailing_only_offset_is_reached": True
|
||||||
},
|
},
|
||||||
|
"max_open_trades": {
|
||||||
|
"max_open_trades": 5
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
HyperoptTools._params_pretty_print(params, 'buy', 'No header', non_optimized)
|
HyperoptTools._params_pretty_print(params, 'buy', 'No header', non_optimized)
|
||||||
@ -327,6 +340,13 @@ def test_params_print(capsys):
|
|||||||
assert re.search('trailing_stop_positive_offset = 0.1 # value loaded.*\n', captured.out)
|
assert re.search('trailing_stop_positive_offset = 0.1 # value loaded.*\n', captured.out)
|
||||||
assert re.search('trailing_only_offset_is_reached = True # value loaded.*\n', captured.out)
|
assert re.search('trailing_only_offset_is_reached = True # value loaded.*\n', captured.out)
|
||||||
|
|
||||||
|
HyperoptTools._params_pretty_print(
|
||||||
|
params, 'max_open_trades', "Max Open Trades:", non_optimized)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
|
||||||
|
assert re.search("# Max Open Trades:", captured.out)
|
||||||
|
assert re.search('max_open_trades = 5 # value loaded.*\n', captured.out)
|
||||||
|
|
||||||
|
|
||||||
def test_hyperopt_serializer():
|
def test_hyperopt_serializer():
|
||||||
|
|
||||||
|
@ -1868,7 +1868,10 @@ def test_get_exit_order_count(fee, is_short):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_update_order_from_ccxt(caplog):
|
def test_update_order_from_ccxt(caplog, time_machine):
|
||||||
|
start = datetime(2023, 1, 1, 4, tzinfo=timezone.utc)
|
||||||
|
time_machine.move_to(start, tick=False)
|
||||||
|
|
||||||
# Most basic order return (only has orderid)
|
# Most basic order return (only has orderid)
|
||||||
o = Order.parse_from_ccxt_object({'id': '1234'}, 'ADA/USDT', 'buy', 20.01, 1234.6)
|
o = Order.parse_from_ccxt_object({'id': '1234'}, 'ADA/USDT', 'buy', 20.01, 1234.6)
|
||||||
assert isinstance(o, Order)
|
assert isinstance(o, Order)
|
||||||
@ -1917,7 +1920,9 @@ def test_update_order_from_ccxt(caplog):
|
|||||||
assert o.filled == 20.0
|
assert o.filled == 20.0
|
||||||
assert o.remaining == 0.0
|
assert o.remaining == 0.0
|
||||||
assert not o.ft_is_open
|
assert not o.ft_is_open
|
||||||
assert o.order_filled_date is not None
|
assert o.order_filled_date == start
|
||||||
|
# Move time
|
||||||
|
time_machine.move_to(start + timedelta(hours=1), tick=False)
|
||||||
|
|
||||||
ccxt_order.update({'id': 'somethingelse'})
|
ccxt_order.update({'id': 'somethingelse'})
|
||||||
with pytest.raises(DependencyException, match=r"Order-id's don't match"):
|
with pytest.raises(DependencyException, match=r"Order-id's don't match"):
|
||||||
@ -1930,6 +1935,12 @@ def test_update_order_from_ccxt(caplog):
|
|||||||
|
|
||||||
# Call regular update - shouldn't fail.
|
# Call regular update - shouldn't fail.
|
||||||
Order.update_orders([o], {'id': '1234'})
|
Order.update_orders([o], {'id': '1234'})
|
||||||
|
assert o.order_filled_date == start
|
||||||
|
|
||||||
|
# Fill order again - shouldn't update filled date
|
||||||
|
ccxt_order.update({'id': '1234'})
|
||||||
|
Order.update_orders([o], ccxt_order)
|
||||||
|
assert o.order_filled_date == start
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
@ -1417,7 +1417,7 @@ def test_api_pair_history(botclient, ohlcv_history):
|
|||||||
"No data for UNITTEST/BTC, 5m in 20200111-20200112 found.")
|
"No data for UNITTEST/BTC, 5m in 20200111-20200112 found.")
|
||||||
|
|
||||||
|
|
||||||
def test_api_plot_config(botclient):
|
def test_api_plot_config(botclient, mocker):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/plot_config")
|
rc = client_get(client, f"{BASE_URI}/plot_config")
|
||||||
@ -1441,6 +1441,21 @@ def test_api_plot_config(botclient):
|
|||||||
assert isinstance(rc.json()['main_plot'], dict)
|
assert isinstance(rc.json()['main_plot'], dict)
|
||||||
assert isinstance(rc.json()['subplots'], dict)
|
assert isinstance(rc.json()['subplots'], dict)
|
||||||
|
|
||||||
|
rc = client_get(client, f"{BASE_URI}/plot_config?strategy=freqai_test_classifier")
|
||||||
|
assert_response(rc)
|
||||||
|
res = rc.json()
|
||||||
|
assert 'target_roi' in res['subplots']
|
||||||
|
assert 'do_predict' in res['subplots']
|
||||||
|
|
||||||
|
rc = client_get(client, f"{BASE_URI}/plot_config?strategy=HyperoptableStrategy")
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json()['subplots'] == {}
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.rpc.api_server.api_v1.get_rpc_optional', return_value=None)
|
||||||
|
|
||||||
|
rc = client_get(client, f"{BASE_URI}/plot_config")
|
||||||
|
assert_response(rc)
|
||||||
|
|
||||||
|
|
||||||
def test_api_strategies(botclient, tmpdir):
|
def test_api_strategies(botclient, tmpdir):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
@ -1553,13 +1568,13 @@ def test_list_available_pairs(botclient):
|
|||||||
client, f"{BASE_URI}/available_pairs?timeframe=1h")
|
client, f"{BASE_URI}/available_pairs?timeframe=1h")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert rc.json()['length'] == 1
|
assert rc.json()['length'] == 1
|
||||||
assert rc.json()['pairs'] == ['XRP/USDT']
|
assert rc.json()['pairs'] == ['XRP/USDT:USDT']
|
||||||
|
|
||||||
rc = client_get(
|
rc = client_get(
|
||||||
client, f"{BASE_URI}/available_pairs?timeframe=1h&candletype=mark")
|
client, f"{BASE_URI}/available_pairs?timeframe=1h&candletype=mark")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert rc.json()['length'] == 2
|
assert rc.json()['length'] == 2
|
||||||
assert rc.json()['pairs'] == ['UNITTEST/USDT', 'XRP/USDT']
|
assert rc.json()['pairs'] == ['UNITTEST/USDT:USDT', 'XRP/USDT:USDT']
|
||||||
assert len(rc.json()['pair_interval']) == 2
|
assert len(rc.json()['pair_interval']) == 2
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,6 +34,11 @@ class HyperoptableStrategy(StrategyTestV3):
|
|||||||
protection_enabled = BooleanParameter(default=True)
|
protection_enabled = BooleanParameter(default=True)
|
||||||
protection_cooldown_lookback = IntParameter([0, 50], default=30)
|
protection_cooldown_lookback = IntParameter([0, 50], default=30)
|
||||||
|
|
||||||
|
# Invalid plot config ...
|
||||||
|
plot_config = {
|
||||||
|
"main_plot": {},
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def protections(self):
|
def protections(self):
|
||||||
prot = []
|
prot = []
|
||||||
|
@ -30,6 +30,9 @@ class StrategyTestV3(IStrategy):
|
|||||||
"0": 0.04
|
"0": 0.04
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Optimal max_open_trades for the strategy
|
||||||
|
max_open_trades = -1
|
||||||
|
|
||||||
# Optimal stoploss designed for the strategy
|
# Optimal stoploss designed for the strategy
|
||||||
stoploss = -0.10
|
stoploss = -0.10
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ from pathlib import Path
|
|||||||
import pytest
|
import pytest
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
@ -175,6 +176,18 @@ def test_strategy_override_stoploss(caplog, default_conf):
|
|||||||
assert log_has("Override strategy 'stoploss' with value in config file: -0.5.", caplog)
|
assert log_has("Override strategy 'stoploss' with value in config file: -0.5.", caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_override_max_open_trades(caplog, default_conf):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
default_conf.update({
|
||||||
|
'strategy': CURRENT_TEST_STRATEGY,
|
||||||
|
'max_open_trades': 7
|
||||||
|
})
|
||||||
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
|
|
||||||
|
assert strategy.max_open_trades == 7
|
||||||
|
assert log_has("Override strategy 'max_open_trades' with value in config file: 7.", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_strategy_override_trailing_stop(caplog, default_conf):
|
def test_strategy_override_trailing_stop(caplog, default_conf):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
default_conf.update({
|
default_conf.update({
|
||||||
@ -349,6 +362,38 @@ def test_strategy_override_use_exit_profit_only(caplog, default_conf):
|
|||||||
assert log_has("Override strategy 'exit_profit_only' with value in config file: True.", caplog)
|
assert log_has("Override strategy 'exit_profit_only' with value in config file: True.", caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_max_open_trades_infinity_from_strategy(caplog, default_conf):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
default_conf.update({
|
||||||
|
'strategy': CURRENT_TEST_STRATEGY,
|
||||||
|
})
|
||||||
|
del default_conf['max_open_trades']
|
||||||
|
|
||||||
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
|
|
||||||
|
# this test assumes -1 set to 'max_open_trades' in CURRENT_TEST_STRATEGY
|
||||||
|
assert strategy.max_open_trades == float('inf')
|
||||||
|
assert default_conf['max_open_trades'] == float('inf')
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_max_open_trades_infinity_from_config(caplog, default_conf, mocker):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
default_conf.update({
|
||||||
|
'strategy': CURRENT_TEST_STRATEGY,
|
||||||
|
'max_open_trades': -1,
|
||||||
|
'exchange': 'binance'
|
||||||
|
})
|
||||||
|
|
||||||
|
configuration = Configuration(args=default_conf)
|
||||||
|
parsed_config = configuration.get_config()
|
||||||
|
|
||||||
|
assert parsed_config['max_open_trades'] == float('inf')
|
||||||
|
|
||||||
|
strategy = StrategyResolver.load_strategy(parsed_config)
|
||||||
|
|
||||||
|
assert strategy.max_open_trades == float('inf')
|
||||||
|
|
||||||
|
|
||||||
@ pytest.mark.filterwarnings("ignore:deprecated")
|
@ pytest.mark.filterwarnings("ignore:deprecated")
|
||||||
def test_missing_implements(default_conf, caplog):
|
def test_missing_implements(default_conf, caplog):
|
||||||
|
|
||||||
@ -438,3 +483,19 @@ def test_strategy_interface_versioning(dataframe_1m, default_conf):
|
|||||||
assert isinstance(exitdf, DataFrame)
|
assert isinstance(exitdf, DataFrame)
|
||||||
assert 'sell' not in exitdf
|
assert 'sell' not in exitdf
|
||||||
assert 'exit_long' in exitdf
|
assert 'exit_long' in exitdf
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_ft_load_params_from_file(mocker, default_conf):
|
||||||
|
default_conf.update({'strategy': 'StrategyTestV2'})
|
||||||
|
del default_conf['max_open_trades']
|
||||||
|
mocker.patch('freqtrade.strategy.hyper.HyperStrategyMixin.load_params_from_file',
|
||||||
|
return_value={
|
||||||
|
'params': {
|
||||||
|
'max_open_trades': {
|
||||||
|
'max_open_trades': -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
|
assert strategy.max_open_trades == float('inf')
|
||||||
|
assert strategy.config['max_open_trades'] == float('inf')
|
||||||
|
58
tests/test_binance_mig.py
Normal file
58
tests/test_binance_mig.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.util.binance_mig import migrate_binance_futures_data, migrate_binance_futures_names
|
||||||
|
from tests.conftest import create_mock_trades_usdt, log_has
|
||||||
|
|
||||||
|
|
||||||
|
def test_binance_mig_data_conversion(default_conf_usdt, tmpdir, testdatadir):
|
||||||
|
|
||||||
|
# call doing nothing (spot mode)
|
||||||
|
migrate_binance_futures_data(default_conf_usdt)
|
||||||
|
default_conf_usdt['trading_mode'] = 'futures'
|
||||||
|
pair_old = 'XRP_USDT'
|
||||||
|
pair_unified = 'XRP_USDT_USDT'
|
||||||
|
futures_src = testdatadir / 'futures'
|
||||||
|
futures_dst = tmpdir / 'futures'
|
||||||
|
futures_dst.mkdir()
|
||||||
|
files = [
|
||||||
|
'-1h-mark.json',
|
||||||
|
'-1h-futures.json',
|
||||||
|
'-8h-funding_rate.json',
|
||||||
|
'-8h-mark.json',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Copy files to tmpdir and rename to old naming
|
||||||
|
for file in files:
|
||||||
|
fn_after = futures_dst / f'{pair_old}{file}'
|
||||||
|
shutil.copy(futures_src / f'{pair_unified}{file}', fn_after)
|
||||||
|
|
||||||
|
default_conf_usdt['datadir'] = Path(tmpdir)
|
||||||
|
# Migrate files to unified namings
|
||||||
|
migrate_binance_futures_data(default_conf_usdt)
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
fn_after = futures_dst / f'{pair_unified}{file}'
|
||||||
|
assert fn_after.exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
def test_binance_mig_db_conversion(default_conf_usdt, fee, caplog):
|
||||||
|
# Does nothing in spot mode
|
||||||
|
migrate_binance_futures_names(default_conf_usdt)
|
||||||
|
|
||||||
|
create_mock_trades_usdt(fee, None)
|
||||||
|
|
||||||
|
for t in Trade.get_trades():
|
||||||
|
t.trading_mode = 'FUTURES'
|
||||||
|
t.exchange = 'binance'
|
||||||
|
Trade.commit()
|
||||||
|
|
||||||
|
default_conf_usdt['trading_mode'] = 'futures'
|
||||||
|
migrate_binance_futures_names(default_conf_usdt)
|
||||||
|
assert log_has('Migrating binance futures pairs in database.', caplog)
|
@ -58,6 +58,7 @@ def test_load_config_incorrect_stake_amount(default_conf) -> None:
|
|||||||
|
|
||||||
def test_load_config_file(default_conf, mocker, caplog) -> None:
|
def test_load_config_file(default_conf, mocker, caplog) -> None:
|
||||||
del default_conf['user_data_dir']
|
del default_conf['user_data_dir']
|
||||||
|
default_conf['datadir'] = str(default_conf['datadir'])
|
||||||
file_mock = mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(
|
file_mock = mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(
|
||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
@ -69,6 +70,7 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
def test_load_config_file_error(default_conf, mocker, caplog) -> None:
|
def test_load_config_file_error(default_conf, mocker, caplog) -> None:
|
||||||
del default_conf['user_data_dir']
|
del default_conf['user_data_dir']
|
||||||
|
default_conf['datadir'] = str(default_conf['datadir'])
|
||||||
filedata = json.dumps(default_conf).replace(
|
filedata = json.dumps(default_conf).replace(
|
||||||
'"stake_amount": 0.001,', '"stake_amount": .001,')
|
'"stake_amount": 0.001,', '"stake_amount": .001,')
|
||||||
mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(read_data=filedata))
|
mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(read_data=filedata))
|
||||||
@ -80,6 +82,7 @@ def test_load_config_file_error(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
def test_load_config_file_error_range(default_conf, mocker, caplog) -> None:
|
def test_load_config_file_error_range(default_conf, mocker, caplog) -> None:
|
||||||
del default_conf['user_data_dir']
|
del default_conf['user_data_dir']
|
||||||
|
default_conf['datadir'] = str(default_conf['datadir'])
|
||||||
filedata = json.dumps(default_conf).replace(
|
filedata = json.dumps(default_conf).replace(
|
||||||
'"stake_amount": 0.001,', '"stake_amount": .001,')
|
'"stake_amount": 0.001,', '"stake_amount": .001,')
|
||||||
mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata))
|
mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata))
|
||||||
@ -238,6 +241,7 @@ def test_print_config(default_conf, mocker, caplog) -> None:
|
|||||||
conf1 = deepcopy(default_conf)
|
conf1 = deepcopy(default_conf)
|
||||||
# Delete non-json elements from default_conf
|
# Delete non-json elements from default_conf
|
||||||
del conf1['user_data_dir']
|
del conf1['user_data_dir']
|
||||||
|
conf1['datadir'] = str(conf1['datadir'])
|
||||||
config_files = [conf1]
|
config_files = [conf1]
|
||||||
|
|
||||||
configsmock = MagicMock(side_effect=config_files)
|
configsmock = MagicMock(side_effect=config_files)
|
||||||
|
@ -45,7 +45,6 @@ def test_init_plotscript(default_conf, mocker, testdatadir):
|
|||||||
default_conf['timerange'] = "20180110-20180112"
|
default_conf['timerange'] = "20180110-20180112"
|
||||||
default_conf['trade_source'] = "file"
|
default_conf['trade_source'] = "file"
|
||||||
default_conf['timeframe'] = "5m"
|
default_conf['timeframe'] = "5m"
|
||||||
default_conf["datadir"] = testdatadir
|
|
||||||
default_conf['exportfilename'] = testdatadir / "backtest-result.json"
|
default_conf['exportfilename'] = testdatadir / "backtest-result.json"
|
||||||
supported_markets = ["TRX/BTC", "ADA/BTC"]
|
supported_markets = ["TRX/BTC", "ADA/BTC"]
|
||||||
ret = init_plotscript(default_conf, supported_markets)
|
ret = init_plotscript(default_conf, supported_markets)
|
||||||
@ -394,7 +393,6 @@ def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir):
|
|||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
|
||||||
default_conf['trade_source'] = 'file'
|
default_conf['trade_source'] = 'file'
|
||||||
default_conf["datadir"] = testdatadir
|
|
||||||
default_conf['exportfilename'] = testdatadir / "backtest-result.json"
|
default_conf['exportfilename'] = testdatadir / "backtest-result.json"
|
||||||
default_conf['indicators1'] = ["sma5", "ema10"]
|
default_conf['indicators1'] = ["sma5", "ema10"]
|
||||||
default_conf['indicators2'] = ["macd"]
|
default_conf['indicators2'] = ["macd"]
|
||||||
@ -451,7 +449,6 @@ def test_start_plot_profit_error(mocker):
|
|||||||
def test_plot_profit(default_conf, mocker, testdatadir):
|
def test_plot_profit(default_conf, mocker, testdatadir):
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
default_conf['trade_source'] = 'file'
|
default_conf['trade_source'] = 'file'
|
||||||
default_conf['datadir'] = testdatadir
|
|
||||||
default_conf['exportfilename'] = testdatadir / 'backtest-result_test_nofile.json'
|
default_conf['exportfilename'] = testdatadir / 'backtest-result_test_nofile.json'
|
||||||
default_conf['pairs'] = ['ETH/BTC', 'LTC/BTC']
|
default_conf['pairs'] = ['ETH/BTC', 'LTC/BTC']
|
||||||
|
|
||||||
|
@ -190,7 +190,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r
|
|||||||
(1, 15, 10, 10000, None, 0), # Below min stake and min_stake > stake_available
|
(1, 15, 10, 10000, None, 0), # Below min stake and min_stake > stake_available
|
||||||
(20, 50, 100, 10000, None, 0), # Below min stake and stake * 1.3 > min_stake
|
(20, 50, 100, 10000, None, 0), # Below min stake and stake * 1.3 > min_stake
|
||||||
(1000, None, 1000, 10000, None, 1000), # No min-stake-amount could be determined
|
(1000, None, 1000, 10000, None, 1000), # No min-stake-amount could be determined
|
||||||
(2000, 15, 2000, 3000, 1500, 500), # Rebuy - resulting in too high stake amount. Adjusting.
|
(2000, 15, 2000, 3000, 1500, 1500), # Rebuy - resulting in too high stake amount. Adjusting.
|
||||||
])
|
])
|
||||||
def test_validate_stake_amount(
|
def test_validate_stake_amount(
|
||||||
mocker,
|
mocker,
|
||||||
|
1
tests/testdata/futures/XRP_USDT_USDT-5m-futures.json
vendored
Normal file
1
tests/testdata/futures/XRP_USDT_USDT-5m-futures.json
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user