Compare commits
160 Commits
use-parque
...
dependabot
Author | SHA1 | Date | |
---|---|---|---|
|
3505fbe783 | ||
|
526943f29e | ||
|
df51111c33 | ||
|
dd8900a1c6 | ||
|
9c2cdd4fb9 | ||
|
c2c97d9f78 | ||
|
f8d89c46e5 | ||
|
1952e453bb | ||
|
77985fa591 | ||
|
a75d891007 | ||
|
dae3f72be7 | ||
|
f03a99918a | ||
|
fe02f611fb | ||
|
1b10a3a2bf | ||
|
92a060c5b4 | ||
|
096fd1916c | ||
|
fb09a16127 | ||
|
7fed0782d5 | ||
|
30fc24bd8c | ||
|
7e3de178e1 | ||
|
0c9c9fff0e | ||
|
b96f6670e3 | ||
|
6e02743256 | ||
|
2b4fa92d09 | ||
|
be250230b6 | ||
|
5d33ffc015 | ||
|
b48498f27f | ||
|
e582d8bacb | ||
|
ff40ee655b | ||
|
57deaad806 | ||
|
7779b82277 | ||
|
2bd2058afa | ||
|
bf7936b0af | ||
|
8236bbfd48 | ||
|
4dc13ac16a | ||
|
eb5423469a | ||
|
43496d7929 | ||
|
92c70b6b90 | ||
|
77897c7d6b | ||
|
531861573a | ||
|
c9b904eb0e | ||
|
372f1cb37f | ||
|
a3acdd5240 | ||
|
e6a125719e | ||
|
78a1551798 | ||
|
6f79d14c9c | ||
|
28d8722fa7 | ||
|
2715b2ccf0 | ||
|
2ea575cb31 | ||
|
1b31c54162 | ||
|
e289c10b6c | ||
|
26ed1ca07c | ||
|
b1e20bcd1e | ||
|
12a73bc151 | ||
|
19e112f399 | ||
|
cccf4f305b | ||
|
dc7e834911 | ||
|
a630799984 | ||
|
916e1bbc7c | ||
|
631cb44f5c | ||
|
367186cc34 | ||
|
92f34f262e | ||
|
5e13b48648 | ||
|
6dfb1a1d14 | ||
|
f8330800d1 | ||
|
3ec7c72da1 | ||
|
355fde3bca | ||
|
fa7c29fe9f | ||
|
861c577138 | ||
|
e062a74e70 | ||
|
c330c493d5 | ||
|
8a49d62068 | ||
|
a642524928 | ||
|
eb96490c99 | ||
|
6282b42741 | ||
|
513df4515b | ||
|
411e21f430 | ||
|
f0b5f95fd6 | ||
|
736c396d98 | ||
|
2860e817bd | ||
|
19b78fbc22 | ||
|
cde432fef0 | ||
|
8ae44c204e | ||
|
ed0e7ead31 | ||
|
3928051baf | ||
|
e35c85000e | ||
|
3cabcabcbd | ||
|
85776db692 | ||
|
ce81af08d8 | ||
|
5aa6c1dfae | ||
|
4f4dfa2a59 | ||
|
90669e0ba9 | ||
|
bc9f6d30c1 | ||
|
4ae2333306 | ||
|
8c63e3dc4f | ||
|
b0dddd35ca | ||
|
96ba75179b | ||
|
2589717375 | ||
|
bc0816aa66 | ||
|
1743ad7946 | ||
|
9367cbcfd3 | ||
|
43a7b9236b | ||
|
4891174a71 | ||
|
8845f765db | ||
|
7e11bce4f4 | ||
|
8955e09175 | ||
|
d13ea71a58 | ||
|
b72f61080b | ||
|
75c31cc8cc | ||
|
1b3d9efedd | ||
|
2f8f60373e | ||
|
55781e7f10 | ||
|
72284317c2 | ||
|
80a27bc0db | ||
|
1c9abd9e35 | ||
|
c14ac8a205 | ||
|
b09fb5826f | ||
|
fb1541bdf6 | ||
|
444d18aa39 | ||
|
91ab4abba8 | ||
|
16057da6cc | ||
|
d97500581d | ||
|
f1e831a7b8 | ||
|
31a396bc25 | ||
|
7cdcd97c26 | ||
|
73b59df77b | ||
|
86aef7cf9d | ||
|
159090c0e7 | ||
|
0cb28f3d82 | ||
|
d0d0cbe1d1 | ||
|
02078456fc | ||
|
01dfb1cba8 | ||
|
ee205ddc86 | ||
|
298f5685ee | ||
|
486d8a48a0 | ||
|
d426077445 | ||
|
9aa455fcd4 | ||
|
d9c8b322ce | ||
|
68154a1f52 | ||
|
f7c1ee6d3e | ||
|
9c6a49436b | ||
|
75464c22f5 | ||
|
cdd44a4005 | ||
|
34313a7af6 | ||
|
c0a57d352f | ||
|
cbdd86d777 | ||
|
281dd7785e | ||
|
ad58bac810 | ||
|
8928d3616a | ||
|
e8cffeeffd | ||
|
76d289f0ce | ||
|
245ae99273 | ||
|
70ad7b42b1 | ||
|
0ece73578c | ||
|
bdf19f1d66 | ||
|
0128b63c1c | ||
|
e16db814fa | ||
|
1132fa6093 | ||
|
b262f0b374 | ||
|
a3dee9350f |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -425,7 +425,7 @@ jobs:
|
|||||||
python setup.py sdist bdist_wheel
|
python setup.py sdist bdist_wheel
|
||||||
|
|
||||||
- name: Publish to PyPI (Test)
|
- name: Publish to PyPI (Test)
|
||||||
uses: pypa/gh-action-pypi-publish@v1.8.1
|
uses: pypa/gh-action-pypi-publish@v1.8.4
|
||||||
if: (github.event_name == 'release')
|
if: (github.event_name == 'release')
|
||||||
with:
|
with:
|
||||||
user: __token__
|
user: __token__
|
||||||
@@ -433,7 +433,7 @@ jobs:
|
|||||||
repository_url: https://test.pypi.org/legacy/
|
repository_url: https://test.pypi.org/legacy/
|
||||||
|
|
||||||
- name: Publish to PyPI
|
- name: Publish to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@v1.8.1
|
uses: pypa/gh-action-pypi-publish@v1.8.4
|
||||||
if: (github.event_name == 'release')
|
if: (github.event_name == 'release')
|
||||||
with:
|
with:
|
||||||
user: __token__
|
user: __token__
|
||||||
|
@@ -13,12 +13,12 @@ repos:
|
|||||||
- id: mypy
|
- id: mypy
|
||||||
exclude: build_helpers
|
exclude: build_helpers
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.3.0.4
|
- types-cachetools==5.3.0.5
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.28.11.15
|
- types-requests==2.28.11.17
|
||||||
- types-tabulate==0.9.0.1
|
- types-tabulate==0.9.0.2
|
||||||
- types-python-dateutil==2.8.19.10
|
- types-python-dateutil==2.8.19.11
|
||||||
- SQLAlchemy==2.0.7
|
- SQLAlchemy==2.0.8
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.10.10-slim-bullseye as base
|
FROM python:3.10.11-slim-bullseye as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
@@ -42,9 +42,9 @@ if [ $? -ne 0 ]; then
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot .
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot .
|
||||||
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai .
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai .
|
||||||
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl .
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQAI_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl .
|
||||||
|
|
||||||
# Tag image for upload and next build step
|
# Tag image for upload and next build step
|
||||||
docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM
|
docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM
|
||||||
|
@@ -58,9 +58,9 @@ fi
|
|||||||
# Tag image for upload and next build step
|
# Tag image for upload and next build step
|
||||||
docker tag freqtrade:$TAG ${CACHE_IMAGE}:$TAG
|
docker tag freqtrade:$TAG ${CACHE_IMAGE}:$TAG
|
||||||
|
|
||||||
docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot .
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot .
|
||||||
docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_FREQAI} -f docker/Dockerfile.freqai .
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG} -t freqtrade:${TAG_FREQAI} -f docker/Dockerfile.freqai .
|
||||||
docker build --cache-from freqtrade:${TAG_FREQAI} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_FREQAI} -t freqtrade:${TAG_FREQAI_RL} -f docker/Dockerfile.freqai_rl .
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQAI} -t freqtrade:${TAG_FREQAI_RL} -f docker/Dockerfile.freqai_rl .
|
||||||
|
|
||||||
docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT
|
docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT
|
||||||
docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI
|
docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI
|
||||||
|
@@ -274,19 +274,20 @@ A backtesting result will look like that:
|
|||||||
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
|
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
|
||||||
| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
|
| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
|
||||||
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
|
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
|
||||||
========================================================= EXIT REASON STATS ==========================================================
|
|
||||||
| Exit Reason | Exits | Wins | Draws | Losses |
|
|
||||||
|:-------------------|--------:|------:|-------:|--------:|
|
|
||||||
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
|
||||||
| stop_loss | 166 | 0 | 0 | 166 |
|
|
||||||
| exit_signal | 56 | 36 | 0 | 20 |
|
|
||||||
| force_exit | 2 | 0 | 0 | 2 |
|
|
||||||
====================================================== LEFT OPEN TRADES REPORT ======================================================
|
====================================================== LEFT OPEN TRADES REPORT ======================================================
|
||||||
| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|
| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|
||||||
|:---------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:|
|
|:---------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:|
|
||||||
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
|
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
|
||||||
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
|
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
|
||||||
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
||||||
|
==================== EXIT REASON STATS ====================
|
||||||
|
| Exit Reason | Exits | Wins | Draws | Losses |
|
||||||
|
|:-------------------|--------:|------:|-------:|--------:|
|
||||||
|
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
||||||
|
| stop_loss | 166 | 0 | 0 | 166 |
|
||||||
|
| exit_signal | 56 | 36 | 0 | 20 |
|
||||||
|
| force_exit | 2 | 0 | 0 | 2 |
|
||||||
|
|
||||||
================== SUMMARY METRICS ==================
|
================== SUMMARY METRICS ==================
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|-----------------------------+---------------------|
|
|-----------------------------+---------------------|
|
||||||
|
@@ -60,10 +60,10 @@ This loop will be repeated again and again until the bot is stopped.
|
|||||||
|
|
||||||
* Load historic data for configured pairlist.
|
* Load historic data for configured pairlist.
|
||||||
* Calls `bot_start()` once.
|
* Calls `bot_start()` once.
|
||||||
* Calls `bot_loop_start()` once.
|
|
||||||
* Calculate indicators (calls `populate_indicators()` once per pair).
|
* Calculate indicators (calls `populate_indicators()` once per pair).
|
||||||
* Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair).
|
* Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair).
|
||||||
* Loops per candle simulating entry and exit points.
|
* Loops per candle simulating entry and exit points.
|
||||||
|
* Calls `bot_loop_start()` strategy callback.
|
||||||
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks.
|
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks.
|
||||||
* Calls `adjust_entry_price()` strategy callback for open entry orders.
|
* Calls `adjust_entry_price()` strategy callback for open entry orders.
|
||||||
* Check for trade entry signals (`enter_long` / `enter_short` columns).
|
* Check for trade entry signals (`enter_long` / `enter_short` columns).
|
||||||
|
@@ -6,8 +6,8 @@ Low level feature engineering is performed in the user strategy within a set of
|
|||||||
|
|
||||||
| Function | Description |
|
| Function | Description |
|
||||||
|---------------|-------------|
|
|---------------|-------------|
|
||||||
| `feature_engineering__expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
|
| `feature_engineering_expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
|
||||||
| `feature_engineering__expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`.
|
| `feature_engineering_expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`.
|
||||||
| `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g., day of the week).
|
| `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g., day of the week).
|
||||||
| `set_freqai_targets()` | Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals.
|
| `set_freqai_targets()` | Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals.
|
||||||
|
|
||||||
@@ -182,11 +182,11 @@ In total, the number of features the user of the presented example strat has cre
|
|||||||
$= 3 * 3 * 3 * 2 * 2 = 108$.
|
$= 3 * 3 * 3 * 2 * 2 = 108$.
|
||||||
|
|
||||||
|
|
||||||
### Gain finer control over `feature_engineering_*` functions with `metadata`
|
### Gain finer control over `feature_engineering_*` functions with `metadata`
|
||||||
|
|
||||||
All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc.
|
All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc.
|
||||||
|
|
||||||
```py
|
```python
|
||||||
def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs):
|
def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs):
|
||||||
if metadata["tf"] == "1h":
|
if metadata["tf"] == "1h":
|
||||||
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
|
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
|
||||||
|
@@ -46,7 +46,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset. <br> **Datatype:** Float. <br> Default: `30`.
|
| `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset. <br> **Datatype:** Float. <br> Default: `30`.
|
||||||
| `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, you should be careful to understand the unorthodox nature of this parameter before employing it. <br> **Datatype:** Boolean. <br> Default: `False` (no reversal).
|
| `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, you should be careful to understand the unorthodox nature of this parameter before employing it. <br> **Datatype:** Boolean. <br> Default: `False` (no reversal).
|
||||||
| `shuffle_after_split` | Split the data into train and test sets, and then shuffle both sets individually. <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `shuffle_after_split` | Split the data into train and test sets, and then shuffle both sets individually. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
| `buffer_train_data_candles` | Cut `buffer_train_data_candles` off the beginning and end of the training data *after* the indicators were populated. The main example use is when predicting maxima and minima, the argrelextrema function cannot know the maxima/minima at the edges of the timerange. To improve model accuracy, it is best to compute argrelextrema on the full timerange and then use this function to cut off the edges (buffer) by the kernel. In another case, if the targets are set to a shifted price movement, this buffer is unnecessary because the shifted candles at the end of the timerange will be NaN and FreqAI will automatically cut those off of the training dataset.<br> **Datatype:** Boolean. <br> Default: `False`.
|
| `buffer_train_data_candles` | Cut `buffer_train_data_candles` off the beginning and end of the training data *after* the indicators were populated. The main example use is when predicting maxima and minima, the argrelextrema function cannot know the maxima/minima at the edges of the timerange. To improve model accuracy, it is best to compute argrelextrema on the full timerange and then use this function to cut off the edges (buffer) by the kernel. In another case, if the targets are set to a shifted price movement, this buffer is unnecessary because the shifted candles at the end of the timerange will be NaN and FreqAI will automatically cut those off of the training dataset.<br> **Datatype:** Integer. <br> Default: `0`.
|
||||||
|
|
||||||
### Data split parameters
|
### Data split parameters
|
||||||
|
|
||||||
|
@@ -55,7 +55,7 @@ where `ReinforcementLearner` will use the templated `ReinforcementLearner` from
|
|||||||
dataframe["&-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 below 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(self, dataframe, **kwargs):
|
def feature_engineering_standard(self, dataframe, **kwargs):
|
||||||
@@ -180,7 +180,7 @@ As you begin to modify the strategy and the prediction model, you will quickly r
|
|||||||
|
|
||||||
# you can use feature values from dataframe
|
# you can use feature values from dataframe
|
||||||
# Assumes the shifted RSI indicator has been generated in the strategy.
|
# Assumes the shifted RSI indicator has been generated in the strategy.
|
||||||
rsi_now = self.raw_features[f"%-rsi-period-10_shift-1_{pair}_"
|
rsi_now = self.raw_features[f"%-rsi-period_10_shift-1_{pair}_"
|
||||||
f"{self.config['timeframe']}"].iloc[self._current_tick]
|
f"{self.config['timeframe']}"].iloc[self._current_tick]
|
||||||
|
|
||||||
# reward agent for entering trades
|
# reward agent for entering trades
|
||||||
|
@@ -128,6 +128,9 @@ The FreqAI specific parameter `label_period_candles` defines the offset (number
|
|||||||
|
|
||||||
You can choose to adopt a continual learning scheme by setting `"continual_learning": true` in the config. By enabling `continual_learning`, after training an initial model from scratch, subsequent trainings will start from the final model state of the preceding training. This gives the new model a "memory" of the previous state. By default, this is set to `False` which means that all new models are trained from scratch, without input from previous models.
|
You can choose to adopt a continual learning scheme by setting `"continual_learning": true` in the config. By enabling `continual_learning`, after training an initial model from scratch, subsequent trainings will start from the final model state of the preceding training. This gives the new model a "memory" of the previous state. By default, this is set to `False` which means that all new models are trained from scratch, without input from previous models.
|
||||||
|
|
||||||
|
???+ danger "Continual learning enforces a constant parameter space"
|
||||||
|
Since `continual_learning` means that the model parameter space *cannot* change between trainings, `principal_component_analysis` is automatically disabled when `continual_learning` is enabled. Hint: PCA changes the parameter space and the number of features, learn more about PCA [here](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis).
|
||||||
|
|
||||||
## Hyperopt
|
## Hyperopt
|
||||||
|
|
||||||
You can hyperopt using the same command as for [typical Freqtrade hyperopt](hyperopt.md):
|
You can hyperopt using the same command as for [typical Freqtrade hyperopt](hyperopt.md):
|
||||||
|
@@ -149,7 +149,7 @@ The below example assumes a timeframe of 1 hour:
|
|||||||
* Locks each pair after selling for an additional 5 candles (`CooldownPeriod`), giving other pairs a chance to get filled.
|
* Locks each pair after selling for an additional 5 candles (`CooldownPeriod`), giving other pairs a chance to get filled.
|
||||||
* Stops trading for 4 hours (`4 * 1h candles`) if the last 2 days (`48 * 1h candles`) had 20 trades, which caused a max-drawdown of more than 20%. (`MaxDrawdown`).
|
* Stops trading for 4 hours (`4 * 1h candles`) if the last 2 days (`48 * 1h candles`) had 20 trades, which caused a max-drawdown of more than 20%. (`MaxDrawdown`).
|
||||||
* Stops trading if more than 4 stoploss occur for all pairs within a 1 day (`24 * 1h candles`) limit (`StoplossGuard`).
|
* Stops trading if more than 4 stoploss occur for all pairs within a 1 day (`24 * 1h candles`) limit (`StoplossGuard`).
|
||||||
* Locks all pairs that had 4 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`).
|
* Locks all pairs that had 2 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`).
|
||||||
* Locks all pairs for 2 candles that had a profit of below 0.01 (<1%) within the last 24h (`24 * 1h candles`), a minimum of 4 trades.
|
* Locks all pairs for 2 candles that had a profit of below 0.01 (<1%) within the last 24h (`24 * 1h candles`), a minimum of 4 trades.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
|
@@ -42,14 +42,14 @@ Enable subscribing to an instance by adding the `external_message_consumer` sect
|
|||||||
| `producers` | **Required.** List of producers <br> **Datatype:** Array.
|
| `producers` | **Required.** List of producers <br> **Datatype:** Array.
|
||||||
| `producers.name` | **Required.** Name of this producer. This name must be used in calls to `get_producer_pairs()` and `get_producer_df()` if more than one producer is used.<br> **Datatype:** string
|
| `producers.name` | **Required.** Name of this producer. This name must be used in calls to `get_producer_pairs()` and `get_producer_df()` if more than one producer is used.<br> **Datatype:** string
|
||||||
| `producers.host` | **Required.** The hostname or IP address from your producer.<br> **Datatype:** string
|
| `producers.host` | **Required.** The hostname or IP address from your producer.<br> **Datatype:** string
|
||||||
| `producers.port` | **Required.** The port matching the above host.<br> **Datatype:** string
|
| `producers.port` | **Required.** The port matching the above host.<br>*Defaults to `8080`.*<br> **Datatype:** Integer
|
||||||
| `producers.secure` | **Optional.** Use ssl in websockets connection. Default False.<br> **Datatype:** string
|
| `producers.secure` | **Optional.** Use ssl in websockets connection. Default False.<br> **Datatype:** string
|
||||||
| `producers.ws_token` | **Required.** `ws_token` as configured on the producer.<br> **Datatype:** string
|
| `producers.ws_token` | **Required.** `ws_token` as configured on the producer.<br> **Datatype:** string
|
||||||
| | **Optional settings**
|
| | **Optional settings**
|
||||||
| `wait_timeout` | Timeout until we ping again if no message is received. <br>*Defaults to `300`.*<br> **Datatype:** Integer - in seconds.
|
| `wait_timeout` | Timeout until we ping again if no message is received. <br>*Defaults to `300`.*<br> **Datatype:** Integer - in seconds.
|
||||||
| `wait_timeout` | Ping timeout <br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
| `ping_timeout` | Ping timeout <br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
||||||
| `sleep_time` | Sleep time before retrying to connect.<br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
| `sleep_time` | Sleep time before retrying to connect.<br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
||||||
| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.<br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.<br>*Defaults to `False`.*<br> **Datatype:** Boolean.
|
||||||
| `message_size_limit` | Size limit per message<br>*Defaults to `8`.*<br> **Datatype:** Integer - Megabytes.
|
| `message_size_limit` | Size limit per message<br>*Defaults to `8`.*<br> **Datatype:** Integer - Megabytes.
|
||||||
|
|
||||||
Instead of (or as well as) calculating indicators in `populate_indicators()` the follower instance listens on the connection to a producer instance's messages (or multiple producer instances in advanced configurations) and requests the producer's most recently analyzed dataframes for each pair in the active whitelist.
|
Instead of (or as well as) calculating indicators in `populate_indicators()` the follower instance listens on the connection to a producer instance's messages (or multiple producer instances in advanced configurations) and requests the producer's most recently analyzed dataframes for each pair in the active whitelist.
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
markdown==3.3.7
|
markdown==3.3.7
|
||||||
mkdocs==1.4.2
|
mkdocs==1.4.2
|
||||||
mkdocs-material==9.1.3
|
mkdocs-material==9.1.5
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==9.10
|
pymdown-extensions==9.10
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
|
@@ -9,9 +9,6 @@ This same command can also be used to update freqUI, should there be a new relea
|
|||||||
|
|
||||||
Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`).
|
Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`).
|
||||||
|
|
||||||
!!! info "Alpha release"
|
|
||||||
FreqUI is still considered an alpha release - if you encounter bugs or inconsistencies please open a [FreqUI issue](https://github.com/freqtrade/frequi/issues/new/choose).
|
|
||||||
|
|
||||||
!!! Note "developers"
|
!!! Note "developers"
|
||||||
Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI.
|
Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI.
|
||||||
|
|
||||||
|
@@ -51,7 +51,8 @@ During hyperopt, this runs only once at startup.
|
|||||||
|
|
||||||
## Bot loop start
|
## Bot loop start
|
||||||
|
|
||||||
A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently).
|
A simple callback which is called once at the start of every bot throttling iteration in dry/live mode (roughly every 5
|
||||||
|
seconds, unless configured differently) or once per candle in backtest/hyperopt mode.
|
||||||
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
|
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
@@ -61,11 +62,12 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
# ... populate_* methods
|
# ... populate_* methods
|
||||||
|
|
||||||
def bot_loop_start(self, **kwargs) -> None:
|
def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
Called at the start of the bot iteration (one loop).
|
Called at the start of the bot iteration (one loop).
|
||||||
Might be used to perform pair-independent tasks
|
Might be used to perform pair-independent tasks
|
||||||
(e.g. gather some remote resource for comparison)
|
(e.g. gather some remote resource for comparison)
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
"""
|
"""
|
||||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
if self.config['runmode'].value in ('live', 'dry_run'):
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = '2023.3.dev'
|
__version__ = '2023.4.dev'
|
||||||
|
|
||||||
if 'dev' in __version__:
|
if 'dev' in __version__:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@@ -204,11 +204,14 @@ def start_list_data(args: Dict[str, Any]) -> None:
|
|||||||
pair, timeframe, candle_type,
|
pair, timeframe, candle_type,
|
||||||
*dhc.ohlcv_data_min_max(pair, timeframe, candle_type)
|
*dhc.ohlcv_data_min_max(pair, timeframe, candle_type)
|
||||||
) for pair, timeframe, candle_type in paircombs]
|
) for pair, timeframe, candle_type in paircombs]
|
||||||
|
|
||||||
print(tabulate([
|
print(tabulate([
|
||||||
(pair, timeframe, candle_type,
|
(pair, timeframe, candle_type,
|
||||||
start.strftime(DATETIME_PRINT_FORMAT),
|
start.strftime(DATETIME_PRINT_FORMAT),
|
||||||
end.strftime(DATETIME_PRINT_FORMAT))
|
end.strftime(DATETIME_PRINT_FORMAT))
|
||||||
for pair, timeframe, candle_type, start, end in paircombs1
|
for pair, timeframe, candle_type, start, end in sorted(
|
||||||
|
paircombs1,
|
||||||
|
key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2]))
|
||||||
],
|
],
|
||||||
headers=("Pair", "Timeframe", "Type", 'From', 'To'),
|
headers=("Pair", "Timeframe", "Type", 'From', 'To'),
|
||||||
tablefmt='psql', stralign='right'))
|
tablefmt='psql', stralign='right'))
|
||||||
|
@@ -36,9 +36,10 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', '
|
|||||||
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
||||||
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
||||||
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
||||||
AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
AVAILABLE_PROTECTIONS = ['CooldownPeriod',
|
||||||
AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5']
|
'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
||||||
AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['feather', 'parquet']
|
AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5', 'feather']
|
||||||
|
AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['parquet']
|
||||||
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
|
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
|
||||||
BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month']
|
BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month']
|
||||||
BACKTEST_CACHE_DEFAULT = 'day'
|
BACKTEST_CACHE_DEFAULT = 'day'
|
||||||
@@ -597,7 +598,7 @@ CONF_SCHEMA = {
|
|||||||
"model_type": {"type": "string", "default": "PPO"},
|
"model_type": {"type": "string", "default": "PPO"},
|
||||||
"policy_type": {"type": "string", "default": "MlpPolicy"},
|
"policy_type": {"type": "string", "default": "MlpPolicy"},
|
||||||
"net_arch": {"type": "array", "default": [128, 128]},
|
"net_arch": {"type": "array", "default": [128, 128]},
|
||||||
"randomize_startinng_position": {"type": "boolean", "default": False},
|
"randomize_starting_position": {"type": "boolean", "default": False},
|
||||||
"model_reward_parameters": {
|
"model_reward_parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@@ -21,6 +21,7 @@ from freqtrade.exchange import Exchange, timeframe_to_seconds
|
|||||||
from freqtrade.exchange.types import OrderBook
|
from freqtrade.exchange.types import OrderBook
|
||||||
from freqtrade.misc import append_candles_to_dataframe
|
from freqtrade.misc import append_candles_to_dataframe
|
||||||
from freqtrade.rpc import RPCManager
|
from freqtrade.rpc import RPCManager
|
||||||
|
from freqtrade.rpc.rpc_types import RPCAnalyzedDFMsg
|
||||||
from freqtrade.util import PeriodicCache
|
from freqtrade.util import PeriodicCache
|
||||||
|
|
||||||
|
|
||||||
@@ -118,8 +119,7 @@ class DataProvider:
|
|||||||
:param new_candle: This is a new candle
|
:param new_candle: This is a new candle
|
||||||
"""
|
"""
|
||||||
if self.__rpc:
|
if self.__rpc:
|
||||||
self.__rpc.send_msg(
|
msg: RPCAnalyzedDFMsg = {
|
||||||
{
|
|
||||||
'type': RPCMessageType.ANALYZED_DF,
|
'type': RPCMessageType.ANALYZED_DF,
|
||||||
'data': {
|
'data': {
|
||||||
'key': pair_key,
|
'key': pair_key,
|
||||||
@@ -127,7 +127,7 @@ class DataProvider:
|
|||||||
'la': datetime.now(timezone.utc)
|
'la': datetime.now(timezone.utc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
self.__rpc.send_msg(msg)
|
||||||
if new_candle:
|
if new_candle:
|
||||||
self.__rpc.send_msg({
|
self.__rpc.send_msg({
|
||||||
'type': RPCMessageType.NEW_CANDLE,
|
'type': RPCMessageType.NEW_CANDLE,
|
||||||
|
@@ -4,7 +4,7 @@ from typing import Optional
|
|||||||
from pandas import DataFrame, read_feather, to_datetime
|
from pandas import DataFrame, read_feather, to_datetime
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList
|
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
|
|
||||||
from .idatahandler import IDataHandler
|
from .idatahandler import IDataHandler
|
||||||
@@ -92,12 +92,11 @@ class FeatherDataHandler(IDataHandler):
|
|||||||
:param data: List of Lists containing trade data,
|
:param data: List of Lists containing trade data,
|
||||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
# filename = self._pair_trades_filename(self._datadir, pair)
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
|
self.create_dir_if_needed(filename)
|
||||||
|
|
||||||
raise NotImplementedError()
|
tradesdata = DataFrame(data, columns=DEFAULT_TRADES_COLUMNS)
|
||||||
# array = pa.array(data)
|
tradesdata.to_feather(filename, compression_level=9, compression='lz4')
|
||||||
# array
|
|
||||||
# feather.write_feather(data, filename)
|
|
||||||
|
|
||||||
def trades_append(self, pair: str, data: TradeList):
|
def trades_append(self, pair: str, data: TradeList):
|
||||||
"""
|
"""
|
||||||
@@ -116,14 +115,13 @@ class FeatherDataHandler(IDataHandler):
|
|||||||
:param timerange: Timerange to load trades for - currently not implemented
|
:param timerange: Timerange to load trades for - currently not implemented
|
||||||
:return: List of trades
|
:return: List of trades
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
# filename = self._pair_trades_filename(self._datadir, pair)
|
if not filename.exists():
|
||||||
# tradesdata = misc.file_load_json(filename)
|
return []
|
||||||
|
|
||||||
# if not tradesdata:
|
tradesdata = read_feather(filename)
|
||||||
# return []
|
|
||||||
|
|
||||||
# return tradesdata
|
return tradesdata.values.tolist()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_file_extension(cls):
|
def _get_file_extension(cls):
|
||||||
|
@@ -8,15 +8,15 @@ from freqtrade.exchange.bitpanda import Bitpanda
|
|||||||
from freqtrade.exchange.bittrex import Bittrex
|
from freqtrade.exchange.bittrex import Bittrex
|
||||||
from freqtrade.exchange.bybit import Bybit
|
from freqtrade.exchange.bybit import Bybit
|
||||||
from freqtrade.exchange.coinbasepro import Coinbasepro
|
from freqtrade.exchange.coinbasepro import Coinbasepro
|
||||||
from freqtrade.exchange.exchange_utils import (amount_to_contract_precision, amount_to_contracts,
|
from freqtrade.exchange.exchange_utils import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision,
|
||||||
amount_to_precision, available_exchanges,
|
amount_to_contracts, amount_to_precision,
|
||||||
ccxt_exchanges, contracts_to_amount,
|
available_exchanges, ccxt_exchanges,
|
||||||
date_minus_candles, is_exchange_known_ccxt,
|
contracts_to_amount, date_minus_candles,
|
||||||
market_is_active, price_to_precision,
|
is_exchange_known_ccxt, market_is_active,
|
||||||
timeframe_to_minutes, timeframe_to_msecs,
|
price_to_precision, timeframe_to_minutes,
|
||||||
timeframe_to_next_date, timeframe_to_prev_date,
|
timeframe_to_msecs, timeframe_to_next_date,
|
||||||
timeframe_to_seconds, validate_exchange,
|
timeframe_to_prev_date, timeframe_to_seconds,
|
||||||
validate_exchanges)
|
validate_exchange, validate_exchanges)
|
||||||
from freqtrade.exchange.gate import Gate
|
from freqtrade.exchange.gate import Gate
|
||||||
from freqtrade.exchange.hitbtc import Hitbtc
|
from freqtrade.exchange.hitbtc import Hitbtc
|
||||||
from freqtrade.exchange.huobi import Huobi
|
from freqtrade.exchange.huobi import Huobi
|
||||||
|
@@ -7,7 +7,6 @@ from typing import Dict, List, Optional, Tuple
|
|||||||
import arrow
|
import arrow
|
||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.constants import BuySell
|
|
||||||
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
|
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
|
||||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
@@ -49,26 +48,6 @@ class Binance(Exchange):
|
|||||||
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
||||||
]
|
]
|
||||||
|
|
||||||
def _get_params(
|
|
||||||
self,
|
|
||||||
side: BuySell,
|
|
||||||
ordertype: str,
|
|
||||||
leverage: float,
|
|
||||||
reduceOnly: bool,
|
|
||||||
time_in_force: str = 'GTC',
|
|
||||||
) -> Dict:
|
|
||||||
params = super()._get_params(side, ordertype, leverage, reduceOnly, time_in_force)
|
|
||||||
if (
|
|
||||||
time_in_force == 'PO'
|
|
||||||
and ordertype != 'market'
|
|
||||||
and self.trading_mode == TradingMode.SPOT
|
|
||||||
# Only spot can do post only orders
|
|
||||||
):
|
|
||||||
params.pop('timeInForce')
|
|
||||||
params['postOnly'] = True
|
|
||||||
|
|
||||||
return params
|
|
||||||
|
|
||||||
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
|
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
|
||||||
tickers = super().get_tickers(symbols=symbols, cached=cached)
|
tickers = super().get_tickers(symbols=symbols, cached=cached)
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -30,13 +30,14 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFun
|
|||||||
RetryableOrderError, TemporaryError)
|
RetryableOrderError, TemporaryError)
|
||||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_credentials, retrier,
|
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_credentials, retrier,
|
||||||
retrier_async)
|
retrier_async)
|
||||||
from freqtrade.exchange.exchange_utils import (CcxtModuleType, amount_to_contract_precision,
|
from freqtrade.exchange.exchange_utils import (ROUND, ROUND_DOWN, ROUND_UP, CcxtModuleType,
|
||||||
amount_to_contracts, amount_to_precision,
|
amount_to_contract_precision, amount_to_contracts,
|
||||||
contracts_to_amount, date_minus_candles,
|
amount_to_precision, contracts_to_amount,
|
||||||
is_exchange_known_ccxt, market_is_active,
|
date_minus_candles, is_exchange_known_ccxt,
|
||||||
price_to_precision, timeframe_to_minutes,
|
market_is_active, price_to_precision,
|
||||||
timeframe_to_msecs, timeframe_to_next_date,
|
timeframe_to_minutes, timeframe_to_msecs,
|
||||||
timeframe_to_prev_date, timeframe_to_seconds)
|
timeframe_to_next_date, timeframe_to_prev_date,
|
||||||
|
timeframe_to_seconds)
|
||||||
from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers
|
from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers
|
||||||
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
|
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
|
||||||
safe_value_fallback2)
|
safe_value_fallback2)
|
||||||
@@ -59,6 +60,7 @@ class Exchange:
|
|||||||
# or by specifying them in the configuration.
|
# or by specifying them in the configuration.
|
||||||
_ft_has_default: Dict = {
|
_ft_has_default: Dict = {
|
||||||
"stoploss_on_exchange": False,
|
"stoploss_on_exchange": False,
|
||||||
|
"stop_price_param": "stopPrice",
|
||||||
"order_time_in_force": ["GTC"],
|
"order_time_in_force": ["GTC"],
|
||||||
"ohlcv_params": {},
|
"ohlcv_params": {},
|
||||||
"ohlcv_candle_limit": 500,
|
"ohlcv_candle_limit": 500,
|
||||||
@@ -80,6 +82,8 @@ class Exchange:
|
|||||||
"fee_cost_in_contracts": False, # Fee cost needs contract conversion
|
"fee_cost_in_contracts": False, # Fee cost needs contract conversion
|
||||||
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
|
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
|
||||||
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
|
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
|
||||||
|
# Override createMarketBuyOrderRequiresPrice where ccxt has it wrong
|
||||||
|
"marketOrderRequiresPrice": False,
|
||||||
}
|
}
|
||||||
_ft_has: Dict = {}
|
_ft_has: Dict = {}
|
||||||
_ft_has_futures: Dict = {}
|
_ft_has_futures: Dict = {}
|
||||||
@@ -205,6 +209,8 @@ class Exchange:
|
|||||||
and self._api_async.session):
|
and self._api_async.session):
|
||||||
logger.debug("Closing async ccxt session.")
|
logger.debug("Closing async ccxt session.")
|
||||||
self.loop.run_until_complete(self._api_async.close())
|
self.loop.run_until_complete(self._api_async.close())
|
||||||
|
if self.loop and not self.loop.is_closed():
|
||||||
|
self.loop.close()
|
||||||
|
|
||||||
def validate_config(self, config):
|
def validate_config(self, config):
|
||||||
# Check if timeframe is available
|
# Check if timeframe is available
|
||||||
@@ -730,12 +736,14 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode)
|
return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode)
|
||||||
|
|
||||||
def price_to_precision(self, pair: str, price: float) -> float:
|
def price_to_precision(self, pair: str, price: float, *, rounding_mode: int = ROUND) -> float:
|
||||||
"""
|
"""
|
||||||
Returns the price rounded up to the precision the Exchange accepts.
|
Returns the price rounded to the precision the Exchange accepts.
|
||||||
Rounds up
|
The default price_rounding_mode in conf is ROUND.
|
||||||
|
For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
|
||||||
"""
|
"""
|
||||||
return price_to_precision(price, self.get_precision_price(pair), self.precisionMode)
|
return price_to_precision(price, self.get_precision_price(pair),
|
||||||
|
self.precisionMode, rounding_mode=rounding_mode)
|
||||||
|
|
||||||
def price_get_one_pip(self, pair: str, price: float) -> float:
|
def price_get_one_pip(self, pair: str, price: float) -> float:
|
||||||
"""
|
"""
|
||||||
@@ -758,12 +766,12 @@ class Exchange:
|
|||||||
return self._get_stake_amount_limit(pair, price, stoploss, 'min', leverage)
|
return self._get_stake_amount_limit(pair, price, stoploss, 'min', leverage)
|
||||||
|
|
||||||
def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float:
|
def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float:
|
||||||
max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max')
|
max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max', leverage)
|
||||||
if max_stake_amount is None:
|
if max_stake_amount is None:
|
||||||
# * Should never be executed
|
# * Should never be executed
|
||||||
raise OperationalException(f'{self.name}.get_max_pair_stake_amount should'
|
raise OperationalException(f'{self.name}.get_max_pair_stake_amount should'
|
||||||
'never set max_stake_amount to None')
|
'never set max_stake_amount to None')
|
||||||
return max_stake_amount / leverage
|
return max_stake_amount
|
||||||
|
|
||||||
def _get_stake_amount_limit(
|
def _get_stake_amount_limit(
|
||||||
self,
|
self,
|
||||||
@@ -781,43 +789,41 @@ class Exchange:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
raise ValueError(f"Can't get market information for symbol {pair}")
|
raise ValueError(f"Can't get market information for symbol {pair}")
|
||||||
|
|
||||||
|
if isMin:
|
||||||
|
# reserve some percent defined in config (5% default) + stoploss
|
||||||
|
margin_reserve: float = 1.0 + self._config.get('amount_reserve_percent',
|
||||||
|
DEFAULT_AMOUNT_RESERVE_PERCENT)
|
||||||
|
stoploss_reserve = (
|
||||||
|
margin_reserve / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
|
||||||
|
)
|
||||||
|
# it should not be more than 50%
|
||||||
|
stoploss_reserve = max(min(stoploss_reserve, 1.5), 1)
|
||||||
|
else:
|
||||||
|
margin_reserve = 1.0
|
||||||
|
stoploss_reserve = 1.0
|
||||||
|
|
||||||
stake_limits = []
|
stake_limits = []
|
||||||
limits = market['limits']
|
limits = market['limits']
|
||||||
if (limits['cost'][limit] is not None):
|
if (limits['cost'][limit] is not None):
|
||||||
stake_limits.append(
|
stake_limits.append(
|
||||||
self._contracts_to_amount(
|
self._contracts_to_amount(pair, limits['cost'][limit]) * stoploss_reserve
|
||||||
pair,
|
|
||||||
limits['cost'][limit]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (limits['amount'][limit] is not None):
|
if (limits['amount'][limit] is not None):
|
||||||
stake_limits.append(
|
stake_limits.append(
|
||||||
self._contracts_to_amount(
|
self._contracts_to_amount(pair, limits['amount'][limit]) * price * margin_reserve
|
||||||
pair,
|
|
||||||
limits['amount'][limit] * price
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not stake_limits:
|
if not stake_limits:
|
||||||
return None if isMin else float('inf')
|
return None if isMin else float('inf')
|
||||||
|
|
||||||
# reserve some percent defined in config (5% default) + stoploss
|
|
||||||
amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent',
|
|
||||||
DEFAULT_AMOUNT_RESERVE_PERCENT)
|
|
||||||
amount_reserve_percent = (
|
|
||||||
amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
|
|
||||||
)
|
|
||||||
# it should not be more than 50%
|
|
||||||
amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1)
|
|
||||||
|
|
||||||
# The value returned should satisfy both limits: for amount (base currency) and
|
# The value returned should satisfy both limits: for amount (base currency) and
|
||||||
# for cost (quote, stake currency), so max() is used here.
|
# for cost (quote, stake currency), so max() is used here.
|
||||||
# See also #2575 at github.
|
# See also #2575 at github.
|
||||||
return self._get_stake_amount_considering_leverage(
|
return self._get_stake_amount_considering_leverage(
|
||||||
max(stake_limits) * amount_reserve_percent,
|
max(stake_limits) if isMin else min(stake_limits),
|
||||||
leverage or 1.0
|
leverage or 1.0
|
||||||
) if isMin else min(stake_limits)
|
)
|
||||||
|
|
||||||
def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float:
|
def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float:
|
||||||
"""
|
"""
|
||||||
@@ -1038,6 +1044,13 @@ class Exchange:
|
|||||||
params.update({'reduceOnly': True})
|
params.update({'reduceOnly': True})
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
def _order_needs_price(self, ordertype: str) -> bool:
|
||||||
|
return (
|
||||||
|
ordertype != 'market'
|
||||||
|
or self._api.options.get("createMarketBuyOrderRequiresPrice", False)
|
||||||
|
or self._ft_has.get('marketOrderRequiresPrice', False)
|
||||||
|
)
|
||||||
|
|
||||||
def create_order(
|
def create_order(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -1060,8 +1073,7 @@ class Exchange:
|
|||||||
try:
|
try:
|
||||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||||
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
|
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
|
||||||
needs_price = (ordertype != 'market'
|
needs_price = self._order_needs_price(ordertype)
|
||||||
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
|
|
||||||
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
||||||
|
|
||||||
if not reduceOnly:
|
if not reduceOnly:
|
||||||
@@ -1104,11 +1116,11 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
if not self._ft_has.get('stoploss_on_exchange'):
|
if not self._ft_has.get('stoploss_on_exchange'):
|
||||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||||
|
price_param = self._ft_has['stop_price_param']
|
||||||
return (
|
return (
|
||||||
order.get('stopPrice', None) is None
|
order.get(price_param, None) is None
|
||||||
or ((side == "sell" and stop_loss > float(order['stopPrice'])) or
|
or ((side == "sell" and stop_loss > float(order[price_param])) or
|
||||||
(side == "buy" and stop_loss < float(order['stopPrice'])))
|
(side == "buy" and stop_loss < float(order[price_param])))
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]:
|
def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]:
|
||||||
@@ -1148,8 +1160,8 @@ class Exchange:
|
|||||||
|
|
||||||
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
|
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
|
||||||
params = self._params.copy()
|
params = self._params.copy()
|
||||||
# Verify if stopPrice works for your exchange!
|
# Verify if stopPrice works for your exchange, else configure stop_price_param
|
||||||
params.update({'stopPrice': stop_price})
|
params.update({self._ft_has['stop_price_param']: stop_price})
|
||||||
return params
|
return params
|
||||||
|
|
||||||
@retrier(retries=0)
|
@retrier(retries=0)
|
||||||
@@ -1175,12 +1187,12 @@ class Exchange:
|
|||||||
|
|
||||||
user_order_type = order_types.get('stoploss', 'market')
|
user_order_type = order_types.get('stoploss', 'market')
|
||||||
ordertype, user_order_type = self._get_stop_order_type(user_order_type)
|
ordertype, user_order_type = self._get_stop_order_type(user_order_type)
|
||||||
|
round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP
|
||||||
stop_price_norm = self.price_to_precision(pair, stop_price)
|
stop_price_norm = self.price_to_precision(pair, stop_price, rounding_mode=round_mode)
|
||||||
limit_rate = None
|
limit_rate = None
|
||||||
if user_order_type == 'limit':
|
if user_order_type == 'limit':
|
||||||
limit_rate = self._get_stop_limit_rate(stop_price, order_types, side)
|
limit_rate = self._get_stop_limit_rate(stop_price, order_types, side)
|
||||||
limit_rate = self.price_to_precision(pair, limit_rate)
|
limit_rate = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode)
|
||||||
|
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
dry_order = self.create_dry_run_order(
|
dry_order = self.create_dry_run_order(
|
||||||
|
@@ -2,11 +2,12 @@
|
|||||||
Exchange support utils
|
Exchange support utils
|
||||||
"""
|
"""
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from math import ceil
|
from math import ceil, floor
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision
|
from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGITS, TICK_SIZE,
|
||||||
|
TRUNCATE, decimal_to_precision)
|
||||||
|
|
||||||
from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED
|
from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED
|
||||||
from freqtrade.util import FtPrecise
|
from freqtrade.util import FtPrecise
|
||||||
@@ -219,35 +220,51 @@ def amount_to_contract_precision(
|
|||||||
return amount
|
return amount
|
||||||
|
|
||||||
|
|
||||||
def price_to_precision(price: float, price_precision: Optional[float],
|
def price_to_precision(
|
||||||
precisionMode: Optional[int]) -> float:
|
price: float,
|
||||||
|
price_precision: Optional[float],
|
||||||
|
precisionMode: Optional[int],
|
||||||
|
*,
|
||||||
|
rounding_mode: int = ROUND,
|
||||||
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Returns the price rounded up to the precision the Exchange accepts.
|
Returns the price rounded to the precision the Exchange accepts.
|
||||||
Partial Re-implementation of ccxt internal method decimal_to_precision(),
|
Partial Re-implementation of ccxt internal method decimal_to_precision(),
|
||||||
which does not support rounding up
|
which does not support rounding up.
|
||||||
|
For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
|
||||||
|
|
||||||
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
|
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
|
||||||
align with amount_to_precision().
|
align with amount_to_precision().
|
||||||
!!! Rounds up
|
|
||||||
:param price: price to convert
|
:param price: price to convert
|
||||||
:param price_precision: price precision to use. Used from markets[pair]['precision']['price']
|
:param price_precision: price precision to use. Used from markets[pair]['precision']['price']
|
||||||
:param precisionMode: precision mode to use. Should be used from precisionMode
|
:param precisionMode: precision mode to use. Should be used from precisionMode
|
||||||
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
|
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
|
||||||
|
:param rounding_mode: rounding mode to use. Defaults to ROUND
|
||||||
:return: price rounded up to the precision the Exchange accepts
|
:return: price rounded up to the precision the Exchange accepts
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if price_precision is not None and precisionMode is not None:
|
if price_precision is not None and precisionMode is not None:
|
||||||
# price = float(decimal_to_precision(price, rounding_mode=ROUND,
|
|
||||||
# precision=price_precision,
|
|
||||||
# counting_mode=self.precisionMode,
|
|
||||||
# ))
|
|
||||||
if precisionMode == TICK_SIZE:
|
if precisionMode == TICK_SIZE:
|
||||||
|
if rounding_mode == ROUND:
|
||||||
|
ticks = price / price_precision
|
||||||
|
rounded_ticks = round(ticks)
|
||||||
|
return rounded_ticks * price_precision
|
||||||
precision = FtPrecise(price_precision)
|
precision = FtPrecise(price_precision)
|
||||||
price_str = FtPrecise(price)
|
price_str = FtPrecise(price)
|
||||||
missing = price_str % precision
|
missing = price_str % precision
|
||||||
if not missing == FtPrecise("0"):
|
if not missing == FtPrecise("0"):
|
||||||
price = round(float(str(price_str - missing + precision)), 14)
|
return round(float(str(price_str - missing + precision)), 14)
|
||||||
else:
|
return price
|
||||||
symbol_prec = price_precision
|
elif precisionMode in (SIGNIFICANT_DIGITS, DECIMAL_PLACES):
|
||||||
big_price = price * pow(10, symbol_prec)
|
ndigits = round(price_precision)
|
||||||
price = ceil(big_price) / pow(10, symbol_prec)
|
if rounding_mode == ROUND:
|
||||||
|
return round(price, ndigits)
|
||||||
|
ticks = price * (10**ndigits)
|
||||||
|
if rounding_mode == ROUND_UP:
|
||||||
|
return ceil(ticks) / (10**ndigits)
|
||||||
|
if rounding_mode == TRUNCATE:
|
||||||
|
return int(ticks) / (10**ndigits)
|
||||||
|
if rounding_mode == ROUND_DOWN:
|
||||||
|
return floor(ticks) / (10**ndigits)
|
||||||
|
raise ValueError(f"Unknown rounding_mode {rounding_mode}")
|
||||||
|
raise ValueError(f"Unknown precisionMode {precisionMode}")
|
||||||
return price
|
return price
|
||||||
|
@@ -5,7 +5,6 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
||||||
from freqtrade.exceptions import OperationalException
|
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.misc import safe_value_fallback2
|
from freqtrade.misc import safe_value_fallback2
|
||||||
|
|
||||||
@@ -28,10 +27,12 @@ class Gate(Exchange):
|
|||||||
"order_time_in_force": ['GTC', 'IOC'],
|
"order_time_in_force": ['GTC', 'IOC'],
|
||||||
"stoploss_order_types": {"limit": "limit"},
|
"stoploss_order_types": {"limit": "limit"},
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
|
"marketOrderRequiresPrice": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"needs_trading_fees": True,
|
"needs_trading_fees": True,
|
||||||
|
"marketOrderRequiresPrice": False,
|
||||||
"tickers_have_bid_ask": False,
|
"tickers_have_bid_ask": False,
|
||||||
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
|
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
|
||||||
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
||||||
@@ -50,14 +51,6 @@ class Gate(Exchange):
|
|||||||
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate_ordertypes(self, order_types: Dict) -> None:
|
|
||||||
|
|
||||||
if self.trading_mode != TradingMode.FUTURES:
|
|
||||||
if any(v == 'market' for k, v in order_types.items()):
|
|
||||||
raise OperationalException(
|
|
||||||
f'Exchange {self.name} does not support market orders.')
|
|
||||||
super().validate_stop_ordertypes(order_types)
|
|
||||||
|
|
||||||
def _get_params(
|
def _get_params(
|
||||||
self,
|
self,
|
||||||
side: BuySell,
|
side: BuySell,
|
||||||
|
@@ -12,6 +12,7 @@ from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, Invali
|
|||||||
OperationalException, TemporaryError)
|
OperationalException, TemporaryError)
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
|
from freqtrade.exchange.exchange_utils import ROUND_DOWN, ROUND_UP
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
|
|
||||||
|
|
||||||
@@ -109,6 +110,7 @@ class Kraken(Exchange):
|
|||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
params.update({'reduceOnly': True})
|
params.update({'reduceOnly': True})
|
||||||
|
|
||||||
|
round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP
|
||||||
if order_types.get('stoploss', 'market') == 'limit':
|
if order_types.get('stoploss', 'market') == 'limit':
|
||||||
ordertype = "stop-loss-limit"
|
ordertype = "stop-loss-limit"
|
||||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||||
@@ -116,11 +118,11 @@ class Kraken(Exchange):
|
|||||||
limit_rate = stop_price * limit_price_pct
|
limit_rate = stop_price * limit_price_pct
|
||||||
else:
|
else:
|
||||||
limit_rate = stop_price * (2 - limit_price_pct)
|
limit_rate = stop_price * (2 - limit_price_pct)
|
||||||
params['price2'] = self.price_to_precision(pair, limit_rate)
|
params['price2'] = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode)
|
||||||
else:
|
else:
|
||||||
ordertype = "stop-loss"
|
ordertype = "stop-loss"
|
||||||
|
|
||||||
stop_price = self.price_to_precision(pair, stop_price)
|
stop_price = self.price_to_precision(pair, stop_price, rounding_mode=round_mode)
|
||||||
|
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
dry_order = self.create_dry_run_order(
|
dry_order = self.create_dry_run_order(
|
||||||
|
@@ -28,6 +28,7 @@ class Okx(Exchange):
|
|||||||
"funding_fee_timeframe": "8h",
|
"funding_fee_timeframe": "8h",
|
||||||
"stoploss_order_types": {"limit": "limit"},
|
"stoploss_order_types": {"limit": "limit"},
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
|
"stop_price_param": "stopLossPrice",
|
||||||
}
|
}
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"tickers_have_quoteVolume": False,
|
"tickers_have_quoteVolume": False,
|
||||||
@@ -162,29 +163,12 @@ class Okx(Exchange):
|
|||||||
return pair_tiers[-1]['maxNotional'] / leverage
|
return pair_tiers[-1]['maxNotional'] / leverage
|
||||||
|
|
||||||
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
|
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
|
||||||
|
params = super()._get_stop_params(side, ordertype, stop_price)
|
||||||
params = self._params.copy()
|
|
||||||
# Verify if stopPrice works for your exchange!
|
|
||||||
params.update({'stopLossPrice': stop_price})
|
|
||||||
|
|
||||||
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
|
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
|
||||||
params['tdMode'] = self.margin_mode.value
|
params['tdMode'] = self.margin_mode.value
|
||||||
params['posSide'] = self._get_posSide(side, True)
|
params['posSide'] = self._get_posSide(side, True)
|
||||||
return params
|
return params
|
||||||
|
|
||||||
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
|
|
||||||
"""
|
|
||||||
OKX uses non-default stoploss price naming.
|
|
||||||
"""
|
|
||||||
if not self._ft_has.get('stoploss_on_exchange'):
|
|
||||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
|
||||||
|
|
||||||
return (
|
|
||||||
order.get('stopLossPrice', None) is None
|
|
||||||
or ((side == "sell" and stop_loss > float(order['stopLossPrice'])) or
|
|
||||||
(side == "buy" and stop_loss < float(order['stopLossPrice'])))
|
|
||||||
)
|
|
||||||
|
|
||||||
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
return self.fetch_dry_run_order(order_id)
|
return self.fetch_dry_run_order(order_id)
|
||||||
|
@@ -66,7 +66,7 @@ class Base3ActionRLEnv(BaseEnvironment):
|
|||||||
elif action == Actions.Sell.value and not self.can_short:
|
elif action == Actions.Sell.value and not self.can_short:
|
||||||
self._update_total_profit()
|
self._update_total_profit()
|
||||||
self._position = Positions.Neutral
|
self._position = Positions.Neutral
|
||||||
trade_type = "neutral"
|
trade_type = "exit"
|
||||||
self._last_trade_tick = None
|
self._last_trade_tick = None
|
||||||
else:
|
else:
|
||||||
print("case not defined")
|
print("case not defined")
|
||||||
@@ -74,7 +74,7 @@ class Base3ActionRLEnv(BaseEnvironment):
|
|||||||
if trade_type is not None:
|
if trade_type is not None:
|
||||||
self.trade_history.append(
|
self.trade_history.append(
|
||||||
{'price': self.current_price(), 'index': self._current_tick,
|
{'price': self.current_price(), 'index': self._current_tick,
|
||||||
'type': trade_type})
|
'type': trade_type, 'profit': self.get_unrealized_profit()})
|
||||||
|
|
||||||
if (self._total_profit < self.max_drawdown or
|
if (self._total_profit < self.max_drawdown or
|
||||||
self._total_unrealized_profit < self.max_drawdown):
|
self._total_unrealized_profit < self.max_drawdown):
|
||||||
|
@@ -52,16 +52,6 @@ class Base4ActionRLEnv(BaseEnvironment):
|
|||||||
|
|
||||||
trade_type = None
|
trade_type = None
|
||||||
if self.is_tradesignal(action):
|
if self.is_tradesignal(action):
|
||||||
"""
|
|
||||||
Action: Neutral, position: Long -> Close Long
|
|
||||||
Action: Neutral, position: Short -> Close Short
|
|
||||||
|
|
||||||
Action: Long, position: Neutral -> Open Long
|
|
||||||
Action: Long, position: Short -> Close Short and Open Long
|
|
||||||
|
|
||||||
Action: Short, position: Neutral -> Open Short
|
|
||||||
Action: Short, position: Long -> Close Long and Open Short
|
|
||||||
"""
|
|
||||||
|
|
||||||
if action == Actions.Neutral.value:
|
if action == Actions.Neutral.value:
|
||||||
self._position = Positions.Neutral
|
self._position = Positions.Neutral
|
||||||
@@ -69,16 +59,16 @@ class Base4ActionRLEnv(BaseEnvironment):
|
|||||||
self._last_trade_tick = None
|
self._last_trade_tick = None
|
||||||
elif action == Actions.Long_enter.value:
|
elif action == Actions.Long_enter.value:
|
||||||
self._position = Positions.Long
|
self._position = Positions.Long
|
||||||
trade_type = "long"
|
trade_type = "enter_long"
|
||||||
self._last_trade_tick = self._current_tick
|
self._last_trade_tick = self._current_tick
|
||||||
elif action == Actions.Short_enter.value:
|
elif action == Actions.Short_enter.value:
|
||||||
self._position = Positions.Short
|
self._position = Positions.Short
|
||||||
trade_type = "short"
|
trade_type = "enter_short"
|
||||||
self._last_trade_tick = self._current_tick
|
self._last_trade_tick = self._current_tick
|
||||||
elif action == Actions.Exit.value:
|
elif action == Actions.Exit.value:
|
||||||
self._update_total_profit()
|
self._update_total_profit()
|
||||||
self._position = Positions.Neutral
|
self._position = Positions.Neutral
|
||||||
trade_type = "neutral"
|
trade_type = "exit"
|
||||||
self._last_trade_tick = None
|
self._last_trade_tick = None
|
||||||
else:
|
else:
|
||||||
print("case not defined")
|
print("case not defined")
|
||||||
@@ -86,7 +76,7 @@ class Base4ActionRLEnv(BaseEnvironment):
|
|||||||
if trade_type is not None:
|
if trade_type is not None:
|
||||||
self.trade_history.append(
|
self.trade_history.append(
|
||||||
{'price': self.current_price(), 'index': self._current_tick,
|
{'price': self.current_price(), 'index': self._current_tick,
|
||||||
'type': trade_type})
|
'type': trade_type, 'profit': self.get_unrealized_profit()})
|
||||||
|
|
||||||
if (self._total_profit < self.max_drawdown or
|
if (self._total_profit < self.max_drawdown or
|
||||||
self._total_unrealized_profit < self.max_drawdown):
|
self._total_unrealized_profit < self.max_drawdown):
|
||||||
|
@@ -53,16 +53,6 @@ class Base5ActionRLEnv(BaseEnvironment):
|
|||||||
|
|
||||||
trade_type = None
|
trade_type = None
|
||||||
if self.is_tradesignal(action):
|
if self.is_tradesignal(action):
|
||||||
"""
|
|
||||||
Action: Neutral, position: Long -> Close Long
|
|
||||||
Action: Neutral, position: Short -> Close Short
|
|
||||||
|
|
||||||
Action: Long, position: Neutral -> Open Long
|
|
||||||
Action: Long, position: Short -> Close Short and Open Long
|
|
||||||
|
|
||||||
Action: Short, position: Neutral -> Open Short
|
|
||||||
Action: Short, position: Long -> Close Long and Open Short
|
|
||||||
"""
|
|
||||||
|
|
||||||
if action == Actions.Neutral.value:
|
if action == Actions.Neutral.value:
|
||||||
self._position = Positions.Neutral
|
self._position = Positions.Neutral
|
||||||
@@ -70,21 +60,21 @@ class Base5ActionRLEnv(BaseEnvironment):
|
|||||||
self._last_trade_tick = None
|
self._last_trade_tick = None
|
||||||
elif action == Actions.Long_enter.value:
|
elif action == Actions.Long_enter.value:
|
||||||
self._position = Positions.Long
|
self._position = Positions.Long
|
||||||
trade_type = "long"
|
trade_type = "enter_long"
|
||||||
self._last_trade_tick = self._current_tick
|
self._last_trade_tick = self._current_tick
|
||||||
elif action == Actions.Short_enter.value:
|
elif action == Actions.Short_enter.value:
|
||||||
self._position = Positions.Short
|
self._position = Positions.Short
|
||||||
trade_type = "short"
|
trade_type = "enter_short"
|
||||||
self._last_trade_tick = self._current_tick
|
self._last_trade_tick = self._current_tick
|
||||||
elif action == Actions.Long_exit.value:
|
elif action == Actions.Long_exit.value:
|
||||||
self._update_total_profit()
|
self._update_total_profit()
|
||||||
self._position = Positions.Neutral
|
self._position = Positions.Neutral
|
||||||
trade_type = "neutral"
|
trade_type = "exit_long"
|
||||||
self._last_trade_tick = None
|
self._last_trade_tick = None
|
||||||
elif action == Actions.Short_exit.value:
|
elif action == Actions.Short_exit.value:
|
||||||
self._update_total_profit()
|
self._update_total_profit()
|
||||||
self._position = Positions.Neutral
|
self._position = Positions.Neutral
|
||||||
trade_type = "neutral"
|
trade_type = "exit_short"
|
||||||
self._last_trade_tick = None
|
self._last_trade_tick = None
|
||||||
else:
|
else:
|
||||||
print("case not defined")
|
print("case not defined")
|
||||||
@@ -92,7 +82,7 @@ class Base5ActionRLEnv(BaseEnvironment):
|
|||||||
if trade_type is not None:
|
if trade_type is not None:
|
||||||
self.trade_history.append(
|
self.trade_history.append(
|
||||||
{'price': self.current_price(), 'index': self._current_tick,
|
{'price': self.current_price(), 'index': self._current_tick,
|
||||||
'type': trade_type})
|
'type': trade_type, 'profit': self.get_unrealized_profit()})
|
||||||
|
|
||||||
if (self._total_profit < self.max_drawdown or
|
if (self._total_profit < self.max_drawdown or
|
||||||
self._total_unrealized_profit < self.max_drawdown):
|
self._total_unrealized_profit < self.max_drawdown):
|
||||||
|
@@ -74,8 +74,8 @@ class FreqaiDataDrawer:
|
|||||||
self.historic_predictions: Dict[str, DataFrame] = {}
|
self.historic_predictions: Dict[str, DataFrame] = {}
|
||||||
self.full_path = full_path
|
self.full_path = full_path
|
||||||
self.historic_predictions_path = Path(self.full_path / "historic_predictions.pkl")
|
self.historic_predictions_path = Path(self.full_path / "historic_predictions.pkl")
|
||||||
self.historic_predictions_folder = Path(self.full_path / "historic_predictions")
|
self.historic_predictions_bkp_path = Path(
|
||||||
self.historic_predictions_bkp_folder = Path(self.full_path / "historic_predictions_backup")
|
self.full_path / "historic_predictions.backup.pkl")
|
||||||
self.pair_dictionary_path = Path(self.full_path / "pair_dictionary.json")
|
self.pair_dictionary_path = Path(self.full_path / "pair_dictionary.json")
|
||||||
self.global_metadata_path = Path(self.full_path / "global_metadata.json")
|
self.global_metadata_path = Path(self.full_path / "global_metadata.json")
|
||||||
self.metric_tracker_path = Path(self.full_path / "metric_tracker.json")
|
self.metric_tracker_path = Path(self.full_path / "metric_tracker.json")
|
||||||
@@ -163,12 +163,11 @@ class FreqaiDataDrawer:
|
|||||||
Locate and load a previously saved historic predictions.
|
Locate and load a previously saved historic predictions.
|
||||||
:return: bool - whether or not the drawer was located
|
:return: bool - whether or not the drawer was located
|
||||||
"""
|
"""
|
||||||
exists = self.historic_predictions_folder.exists()
|
exists = self.historic_predictions_path.is_file()
|
||||||
convert = self.historic_predictions_path.is_file()
|
|
||||||
|
|
||||||
if exists:
|
if exists:
|
||||||
try:
|
try:
|
||||||
self.load_historic_predictions_from_folder()
|
with self.historic_predictions_path.open("rb") as fp:
|
||||||
|
self.historic_predictions = cloudpickle.load(fp)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Found existing historic predictions at {self.full_path}, but beware "
|
f"Found existing historic predictions at {self.full_path}, but beware "
|
||||||
"that statistics may be inaccurate if the bot has been offline for "
|
"that statistics may be inaccurate if the bot has been offline for "
|
||||||
@@ -176,54 +175,25 @@ class FreqaiDataDrawer:
|
|||||||
)
|
)
|
||||||
except EOFError:
|
except EOFError:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'Historical prediction files were corrupted. Trying to load backup files.')
|
'Historical prediction file was corrupted. Trying to load backup file.')
|
||||||
self.load_historic_predictions_from_folder()
|
with self.historic_predictions_bkp_path.open("rb") as fp:
|
||||||
logger.warning('FreqAI successfully loaded the backup '
|
self.historic_predictions = cloudpickle.load(fp)
|
||||||
'historical predictions files.')
|
logger.warning('FreqAI successfully loaded the backup historical predictions file.')
|
||||||
|
|
||||||
elif not exists and convert:
|
|
||||||
logger.info("Converting your historic predictions pkl to parquet"
|
|
||||||
"to improve performance.")
|
|
||||||
with Path.open(self.historic_predictions_path, "rb") as fp:
|
|
||||||
self.historic_predictions = cloudpickle.load(fp)
|
|
||||||
self.save_historic_predictions_to_disk()
|
|
||||||
exists = True
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.info("Could not find existing historic_predictions, starting from scratch")
|
||||||
f"Follower could not find historic predictions at {self.full_path} "
|
|
||||||
"sending null values back to strategy"
|
|
||||||
)
|
|
||||||
|
|
||||||
return exists
|
return exists
|
||||||
|
|
||||||
def load_historic_predictions_from_folder(self):
|
|
||||||
"""
|
|
||||||
Try to build the historic_predictions dictionary from parquet
|
|
||||||
files in the historic_predictions_folder
|
|
||||||
"""
|
|
||||||
for file_path in self.historic_predictions_folder.glob("*.parquet"):
|
|
||||||
key = file_path.stem
|
|
||||||
key.replace("_", "/")
|
|
||||||
self.historic_predictions[key] = pd.read_parquet(file_path)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
def save_historic_predictions_to_disk(self):
|
def save_historic_predictions_to_disk(self):
|
||||||
"""
|
"""
|
||||||
Save historic predictions pickle to disk
|
Save historic predictions pickle to disk
|
||||||
"""
|
"""
|
||||||
|
with self.historic_predictions_path.open("wb") as fp:
|
||||||
self.historic_predictions_folder.mkdir(parents=True, exist_ok=True)
|
cloudpickle.dump(self.historic_predictions, fp, protocol=cloudpickle.DEFAULT_PROTOCOL)
|
||||||
for key, value in self.historic_predictions.items():
|
|
||||||
key = key.replace("/", "_")
|
|
||||||
# pytest.set_trace()
|
|
||||||
filename = Path(self.historic_predictions_folder / f"{key}.parquet")
|
|
||||||
value.to_parquet(filename)
|
|
||||||
|
|
||||||
# create a backup
|
# create a backup
|
||||||
shutil.copytree(self.historic_predictions_folder,
|
shutil.copy(self.historic_predictions_path, self.historic_predictions_bkp_path)
|
||||||
self.historic_predictions_bkp_folder, dirs_exist_ok=True)
|
|
||||||
|
|
||||||
def save_metric_tracker_to_disk(self):
|
def save_metric_tracker_to_disk(self):
|
||||||
"""
|
"""
|
||||||
@@ -705,7 +675,7 @@ class FreqaiDataDrawer:
|
|||||||
Returns timerange information based on historic predictions file
|
Returns timerange information based on historic predictions file
|
||||||
:return: timerange calculated from saved live data
|
:return: timerange calculated from saved live data
|
||||||
"""
|
"""
|
||||||
if not self.historic_predictions_folder.exists():
|
if not self.historic_predictions_path.is_file():
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'Historic predictions not found. Historic predictions data is required '
|
'Historic predictions not found. Historic predictions data is required '
|
||||||
'to run backtest with the freqai-backtest-live-models option '
|
'to run backtest with the freqai-backtest-live-models option '
|
||||||
|
@@ -1291,7 +1291,7 @@ class FreqaiDataKitchen:
|
|||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def use_strategy_to_populate_indicators(
|
def use_strategy_to_populate_indicators( # noqa: C901
|
||||||
self,
|
self,
|
||||||
strategy: IStrategy,
|
strategy: IStrategy,
|
||||||
corr_dataframes: dict = {},
|
corr_dataframes: dict = {},
|
||||||
@@ -1362,12 +1362,12 @@ class FreqaiDataKitchen:
|
|||||||
dataframe = self.populate_features(dataframe.copy(), corr_pair, strategy,
|
dataframe = self.populate_features(dataframe.copy(), corr_pair, strategy,
|
||||||
corr_dataframes, base_dataframes, True)
|
corr_dataframes, base_dataframes, True)
|
||||||
|
|
||||||
dataframe = strategy.set_freqai_targets(dataframe.copy(), metadata=metadata)
|
if self.live:
|
||||||
|
dataframe = strategy.set_freqai_targets(dataframe.copy(), metadata=metadata)
|
||||||
|
dataframe = self.remove_special_chars_from_feature_names(dataframe)
|
||||||
|
|
||||||
self.get_unique_classes_from_labels(dataframe)
|
self.get_unique_classes_from_labels(dataframe)
|
||||||
|
|
||||||
dataframe = self.remove_special_chars_from_feature_names(dataframe)
|
|
||||||
|
|
||||||
if self.config.get('reduce_df_footprint', False):
|
if self.config.get('reduce_df_footprint', False):
|
||||||
dataframe = reduce_dataframe_footprint(dataframe)
|
dataframe = reduce_dataframe_footprint(dataframe)
|
||||||
|
|
||||||
|
@@ -105,6 +105,9 @@ class IFreqaiModel(ABC):
|
|||||||
self.max_system_threads = max(int(psutil.cpu_count() * 2 - 2), 1)
|
self.max_system_threads = max(int(psutil.cpu_count() * 2 - 2), 1)
|
||||||
self.can_short = True # overridden in start() with strategy.can_short
|
self.can_short = True # overridden in start() with strategy.can_short
|
||||||
self.model: Any = None
|
self.model: Any = None
|
||||||
|
if self.ft_params.get('principal_component_analysis', False) and self.continual_learning:
|
||||||
|
self.ft_params.update({'principal_component_analysis': False})
|
||||||
|
logger.warning('User tried to use PCA with continual learning. Deactivating PCA.')
|
||||||
|
|
||||||
record_params(config, self.full_path)
|
record_params(config, self.full_path)
|
||||||
|
|
||||||
@@ -154,8 +157,7 @@ class IFreqaiModel(ABC):
|
|||||||
dk = self.start_backtesting(dataframe, metadata, self.dk, strategy)
|
dk = self.start_backtesting(dataframe, metadata, self.dk, strategy)
|
||||||
dataframe = dk.remove_features_from_df(dk.return_dataframe)
|
dataframe = dk.remove_features_from_df(dk.return_dataframe)
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info("Backtesting using historic predictions (live models)")
|
||||||
"Backtesting using historic predictions (live models)")
|
|
||||||
dk = self.start_backtesting_from_historic_predictions(
|
dk = self.start_backtesting_from_historic_predictions(
|
||||||
dataframe, metadata, self.dk)
|
dataframe, metadata, self.dk)
|
||||||
dataframe = dk.return_dataframe
|
dataframe = dk.return_dataframe
|
||||||
@@ -304,7 +306,7 @@ class IFreqaiModel(ABC):
|
|||||||
if check_features:
|
if check_features:
|
||||||
self.dd.load_metadata(dk)
|
self.dd.load_metadata(dk)
|
||||||
dataframe_dummy_features = self.dk.use_strategy_to_populate_indicators(
|
dataframe_dummy_features = self.dk.use_strategy_to_populate_indicators(
|
||||||
strategy, prediction_dataframe=dataframe.tail(1), pair=metadata["pair"]
|
strategy, prediction_dataframe=dataframe.tail(1), pair=pair
|
||||||
)
|
)
|
||||||
dk.find_features(dataframe_dummy_features)
|
dk.find_features(dataframe_dummy_features)
|
||||||
self.check_if_feature_list_matches_strategy(dk)
|
self.check_if_feature_list_matches_strategy(dk)
|
||||||
@@ -314,7 +316,7 @@ class IFreqaiModel(ABC):
|
|||||||
else:
|
else:
|
||||||
if populate_indicators:
|
if populate_indicators:
|
||||||
dataframe = self.dk.use_strategy_to_populate_indicators(
|
dataframe = self.dk.use_strategy_to_populate_indicators(
|
||||||
strategy, prediction_dataframe=dataframe, pair=metadata["pair"]
|
strategy, prediction_dataframe=dataframe, pair=pair
|
||||||
)
|
)
|
||||||
populate_indicators = False
|
populate_indicators = False
|
||||||
|
|
||||||
@@ -330,6 +332,10 @@ class IFreqaiModel(ABC):
|
|||||||
dataframe_train = dk.slice_dataframe(tr_train, dataframe_base_train)
|
dataframe_train = dk.slice_dataframe(tr_train, dataframe_base_train)
|
||||||
dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe_base_backtest)
|
dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe_base_backtest)
|
||||||
|
|
||||||
|
dataframe_train = dk.remove_special_chars_from_feature_names(dataframe_train)
|
||||||
|
dataframe_backtest = dk.remove_special_chars_from_feature_names(dataframe_backtest)
|
||||||
|
dk.get_unique_classes_from_labels(dataframe_train)
|
||||||
|
|
||||||
if not self.model_exists(dk):
|
if not self.model_exists(dk):
|
||||||
dk.find_features(dataframe_train)
|
dk.find_features(dataframe_train)
|
||||||
dk.find_labels(dataframe_train)
|
dk.find_labels(dataframe_train)
|
||||||
|
@@ -21,7 +21,8 @@ from freqtrade.enums import (ExitCheckTuple, ExitType, RPCMessageType, RunMode,
|
|||||||
State, TradingMode)
|
State, TradingMode)
|
||||||
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
||||||
InvalidOrderException, PricingError)
|
InvalidOrderException, PricingError)
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds
|
from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, timeframe_to_minutes, timeframe_to_next_date,
|
||||||
|
timeframe_to_seconds)
|
||||||
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
|
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
|
||||||
from freqtrade.mixins import LoggingMixin
|
from freqtrade.mixins import LoggingMixin
|
||||||
from freqtrade.persistence import Order, PairLocks, Trade, init_db
|
from freqtrade.persistence import Order, PairLocks, Trade, init_db
|
||||||
@@ -30,6 +31,8 @@ from freqtrade.plugins.protectionmanager import ProtectionManager
|
|||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
from freqtrade.rpc import RPCManager
|
from freqtrade.rpc import RPCManager
|
||||||
from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer
|
from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer
|
||||||
|
from freqtrade.rpc.rpc_types import (RPCBuyMsg, RPCCancelMsg, RPCProtectionMsg, RPCSellCancelMsg,
|
||||||
|
RPCSellMsg)
|
||||||
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
|
||||||
@@ -212,7 +215,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
|
self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
|
||||||
self.strategy.gather_informative_pairs())
|
self.strategy.gather_informative_pairs())
|
||||||
|
|
||||||
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
|
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
|
||||||
|
current_time=datetime.now(timezone.utc))
|
||||||
|
|
||||||
self.strategy.analyze(self.active_pair_whitelist)
|
self.strategy.analyze(self.active_pair_whitelist)
|
||||||
|
|
||||||
@@ -850,11 +854,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.info(f"Canceling stoploss on exchange for {trade}")
|
logger.info(f"Canceling stoploss on exchange for {trade}")
|
||||||
co = self.exchange.cancel_stoploss_order_with_result(
|
co = self.exchange.cancel_stoploss_order_with_result(
|
||||||
trade.stoploss_order_id, trade.pair, trade.amount)
|
trade.stoploss_order_id, trade.pair, trade.amount)
|
||||||
trade.update_order(co)
|
self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True)
|
||||||
|
|
||||||
# Reset stoploss order id.
|
# Reset stoploss order id.
|
||||||
trade.stoploss_order_id = None
|
trade.stoploss_order_id = None
|
||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id} "
|
||||||
|
f"for pair {trade.pair}")
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
def get_valid_enter_price_and_stake(
|
def get_valid_enter_price_and_stake(
|
||||||
@@ -941,12 +947,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
return enter_limit_requested, stake_amount, leverage
|
return enter_limit_requested, stake_amount, leverage
|
||||||
|
|
||||||
def _notify_enter(self, trade: Trade, order: Order, order_type: Optional[str] = None,
|
def _notify_enter(self, trade: Trade, order: Order, order_type: str,
|
||||||
fill: bool = False, sub_trade: bool = False) -> None:
|
fill: bool = False, sub_trade: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Sends rpc notification when a entry order occurred.
|
Sends rpc notification when a entry order occurred.
|
||||||
"""
|
"""
|
||||||
msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY
|
|
||||||
open_rate = order.safe_price
|
open_rate = order.safe_price
|
||||||
|
|
||||||
if open_rate is None:
|
if open_rate is None:
|
||||||
@@ -957,9 +962,9 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
current_rate = self.exchange.get_rate(
|
current_rate = self.exchange.get_rate(
|
||||||
trade.pair, side='entry', is_short=trade.is_short, refresh=False)
|
trade.pair, side='entry', is_short=trade.is_short, refresh=False)
|
||||||
|
|
||||||
msg = {
|
msg: RPCBuyMsg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'type': msg_type,
|
'type': RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY,
|
||||||
'buy_tag': trade.enter_tag,
|
'buy_tag': trade.enter_tag,
|
||||||
'enter_tag': trade.enter_tag,
|
'enter_tag': trade.enter_tag,
|
||||||
'exchange': trade.exchange.capitalize(),
|
'exchange': trade.exchange.capitalize(),
|
||||||
@@ -971,6 +976,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'order_type': order_type,
|
'order_type': order_type,
|
||||||
'stake_amount': trade.stake_amount,
|
'stake_amount': trade.stake_amount,
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
|
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
||||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||||
'amount': order.safe_amount_after_fee if fill else (order.amount or trade.amount),
|
'amount': order.safe_amount_after_fee if fill else (order.amount or trade.amount),
|
||||||
'open_date': trade.open_date or datetime.utcnow(),
|
'open_date': trade.open_date or datetime.utcnow(),
|
||||||
@@ -989,7 +995,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
current_rate = self.exchange.get_rate(
|
current_rate = self.exchange.get_rate(
|
||||||
trade.pair, side='entry', is_short=trade.is_short, refresh=False)
|
trade.pair, side='entry', is_short=trade.is_short, refresh=False)
|
||||||
|
|
||||||
msg = {
|
msg: RPCCancelMsg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'type': RPCMessageType.ENTRY_CANCEL,
|
'type': RPCMessageType.ENTRY_CANCEL,
|
||||||
'buy_tag': trade.enter_tag,
|
'buy_tag': trade.enter_tag,
|
||||||
@@ -1001,7 +1007,9 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'limit': trade.open_rate,
|
'limit': trade.open_rate,
|
||||||
'order_type': order_type,
|
'order_type': order_type,
|
||||||
'stake_amount': trade.stake_amount,
|
'stake_amount': trade.stake_amount,
|
||||||
|
'open_rate': trade.open_rate,
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
|
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
||||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||||
'amount': trade.amount,
|
'amount': trade.amount,
|
||||||
'open_date': trade.open_date,
|
'open_date': trade.open_date,
|
||||||
@@ -1165,7 +1173,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.warning('Unable to fetch stoploss order: %s', exception)
|
logger.warning('Unable to fetch stoploss order: %s', exception)
|
||||||
|
|
||||||
if stoploss_order:
|
if stoploss_order:
|
||||||
trade.update_order(stoploss_order)
|
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
|
||||||
|
stoploss_order=True)
|
||||||
|
|
||||||
# We check if stoploss order is fulfilled
|
# We check if stoploss order is fulfilled
|
||||||
if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
|
if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
|
||||||
@@ -1229,7 +1238,9 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:param order: Current on exchange stoploss order
|
:param order: Current on exchange stoploss order
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stoploss_or_liquidation)
|
stoploss_norm = self.exchange.price_to_precision(
|
||||||
|
trade.pair, trade.stoploss_or_liquidation,
|
||||||
|
rounding_mode=ROUND_DOWN if trade.is_short else ROUND_UP)
|
||||||
|
|
||||||
if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side):
|
if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side):
|
||||||
# we check if the update is necessary
|
# we check if the update is necessary
|
||||||
@@ -1239,13 +1250,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# cancelling the current stoploss on exchange first
|
# cancelling the current stoploss on exchange first
|
||||||
logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} "
|
logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} "
|
||||||
f"(orderid:{order['id']}) in order to add another one ...")
|
f"(orderid:{order['id']}) in order to add another one ...")
|
||||||
try:
|
|
||||||
co = self.exchange.cancel_stoploss_order_with_result(order['id'], trade.pair,
|
self.cancel_stoploss_on_exchange(trade)
|
||||||
trade.amount)
|
|
||||||
trade.update_order(co)
|
|
||||||
except InvalidOrderException:
|
|
||||||
logger.exception(f"Could not cancel stoploss order {order['id']} "
|
|
||||||
f"for pair {trade.pair}")
|
|
||||||
|
|
||||||
# Create new stoploss order
|
# Create new stoploss order
|
||||||
if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):
|
if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):
|
||||||
@@ -1477,8 +1483,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
order = self.exchange.cancel_order_with_result(order['id'], trade.pair,
|
order = self.exchange.cancel_order_with_result(
|
||||||
trade.amount)
|
order['id'], trade.pair, trade.amount)
|
||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
|
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
|
||||||
@@ -1490,17 +1496,18 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Order might be filled above in odd timing issues.
|
# Order might be filled above in odd timing issues.
|
||||||
if order.get('status') in ('canceled', 'cancelled'):
|
if order.get('status') in ('canceled', 'cancelled'):
|
||||||
trade.exit_reason = None
|
trade.exit_reason = None
|
||||||
|
trade.open_order_id = None
|
||||||
else:
|
else:
|
||||||
trade.exit_reason = exit_reason_prev
|
trade.exit_reason = exit_reason_prev
|
||||||
cancelled = True
|
cancelled = True
|
||||||
else:
|
else:
|
||||||
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||||
trade.exit_reason = None
|
trade.exit_reason = None
|
||||||
|
trade.open_order_id = None
|
||||||
|
|
||||||
self.update_trade_state(trade, trade.open_order_id, order)
|
self.update_trade_state(trade, trade.open_order_id, order)
|
||||||
|
|
||||||
logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
|
logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
|
||||||
trade.open_order_id = None
|
|
||||||
trade.close_rate = None
|
trade.close_rate = None
|
||||||
trade.close_rate_requested = None
|
trade.close_rate_requested = None
|
||||||
|
|
||||||
@@ -1666,7 +1673,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
amount = trade.amount
|
amount = trade.amount
|
||||||
gain = "profit" if profit_ratio > 0 else "loss"
|
gain = "profit" if profit_ratio > 0 else "loss"
|
||||||
|
|
||||||
msg = {
|
msg: RPCSellMsg = {
|
||||||
'type': (RPCMessageType.EXIT_FILL if fill
|
'type': (RPCMessageType.EXIT_FILL if fill
|
||||||
else RPCMessageType.EXIT),
|
else RPCMessageType.EXIT),
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
@@ -1692,6 +1699,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'close_date': trade.close_date or datetime.utcnow(),
|
'close_date': trade.close_date or datetime.utcnow(),
|
||||||
'stake_amount': trade.stake_amount,
|
'stake_amount': trade.stake_amount,
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
|
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
||||||
'fiat_currency': self.config.get('fiat_display_currency'),
|
'fiat_currency': self.config.get('fiat_display_currency'),
|
||||||
'sub_trade': sub_trade,
|
'sub_trade': sub_trade,
|
||||||
'cumulative_profit': trade.realized_profit,
|
'cumulative_profit': trade.realized_profit,
|
||||||
@@ -1722,7 +1730,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
profit_ratio = trade.calc_profit_ratio(profit_rate)
|
profit_ratio = trade.calc_profit_ratio(profit_rate)
|
||||||
gain = "profit" if profit_ratio > 0 else "loss"
|
gain = "profit" if profit_ratio > 0 else "loss"
|
||||||
|
|
||||||
msg = {
|
msg: RPCSellCancelMsg = {
|
||||||
'type': RPCMessageType.EXIT_CANCEL,
|
'type': RPCMessageType.EXIT_CANCEL,
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'exchange': trade.exchange.capitalize(),
|
'exchange': trade.exchange.capitalize(),
|
||||||
@@ -1744,6 +1752,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'open_date': trade.open_date,
|
'open_date': trade.open_date,
|
||||||
'close_date': trade.close_date or datetime.now(timezone.utc),
|
'close_date': trade.close_date or datetime.now(timezone.utc),
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
|
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
||||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||||
'reason': reason,
|
'reason': reason,
|
||||||
'sub_trade': sub_trade,
|
'sub_trade': sub_trade,
|
||||||
@@ -1775,11 +1784,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Update trade with order values
|
# Update trade with order values
|
||||||
logger.info(f'Found open order for {trade}')
|
if not stoploss_order:
|
||||||
|
logger.info(f'Found open order for {trade}')
|
||||||
try:
|
try:
|
||||||
order = action_order or self.exchange.fetch_order_or_stoploss_order(order_id,
|
order = action_order or self.exchange.fetch_order_or_stoploss_order(
|
||||||
trade.pair,
|
order_id, trade.pair, stoploss_order)
|
||||||
stoploss_order)
|
|
||||||
except InvalidOrderException as exception:
|
except InvalidOrderException as exception:
|
||||||
logger.warning('Unable to fetch order %s: %s', order_id, exception)
|
logger.warning('Unable to fetch order %s: %s', order_id, exception)
|
||||||
return False
|
return False
|
||||||
@@ -1808,7 +1817,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# TODO: should shorting/leverage be supported by Edge,
|
# TODO: should shorting/leverage be supported by Edge,
|
||||||
# then this will need to be fixed.
|
# then this will need to be fixed.
|
||||||
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
|
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
|
||||||
if order.get('side') == trade.entry_side or trade.amount > 0:
|
if order.get('side') == trade.entry_side or (trade.amount > 0 and trade.is_open):
|
||||||
# Must also run for partial exits
|
# Must also run for partial exits
|
||||||
# TODO: Margin will need to use interest_rate as well.
|
# TODO: Margin will need to use interest_rate as well.
|
||||||
# interest_rate = self.exchange.get_interest_rate()
|
# interest_rate = self.exchange.get_interest_rate()
|
||||||
@@ -1844,21 +1853,27 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.handle_protections(trade.pair, trade.trade_direction)
|
self.handle_protections(trade.pair, trade.trade_direction)
|
||||||
elif send_msg and not trade.open_order_id and not stoploss_order:
|
elif send_msg and not trade.open_order_id and not stoploss_order:
|
||||||
# Enter fill
|
# Enter fill
|
||||||
self._notify_enter(trade, order, fill=True, sub_trade=sub_trade)
|
self._notify_enter(trade, order, order.order_type, fill=True, sub_trade=sub_trade)
|
||||||
|
|
||||||
def handle_protections(self, pair: str, side: LongShort) -> None:
|
def handle_protections(self, pair: str, side: LongShort) -> None:
|
||||||
# Lock pair for one candle to prevent immediate rebuys
|
# Lock pair for one candle to prevent immediate rebuys
|
||||||
self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason='Auto lock')
|
self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason='Auto lock')
|
||||||
prot_trig = self.protections.stop_per_pair(pair, side=side)
|
prot_trig = self.protections.stop_per_pair(pair, side=side)
|
||||||
if prot_trig:
|
if prot_trig:
|
||||||
msg = {'type': RPCMessageType.PROTECTION_TRIGGER, }
|
msg: RPCProtectionMsg = {
|
||||||
msg.update(prot_trig.to_json())
|
'type': RPCMessageType.PROTECTION_TRIGGER,
|
||||||
|
'base_currency': self.exchange.get_pair_base_currency(prot_trig.pair),
|
||||||
|
**prot_trig.to_json() # type: ignore
|
||||||
|
}
|
||||||
self.rpc.send_msg(msg)
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
prot_trig_glb = self.protections.global_stop(side=side)
|
prot_trig_glb = self.protections.global_stop(side=side)
|
||||||
if prot_trig_glb:
|
if prot_trig_glb:
|
||||||
msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, }
|
msg = {
|
||||||
msg.update(prot_trig_glb.to_json())
|
'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
|
||||||
|
'base_currency': self.exchange.get_pair_base_currency(prot_trig_glb.pair),
|
||||||
|
**prot_trig_glb.to_json() # type: ignore
|
||||||
|
}
|
||||||
self.rpc.send_msg(msg)
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,
|
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,
|
||||||
|
@@ -203,9 +203,10 @@ class Backtesting:
|
|||||||
# since a "perfect" stoploss-exit is assumed anyway
|
# since a "perfect" stoploss-exit is assumed anyway
|
||||||
# And the regular "stoploss" function would not apply to that case
|
# And the regular "stoploss" function would not apply to that case
|
||||||
self.strategy.order_types['stoploss_on_exchange'] = False
|
self.strategy.order_types['stoploss_on_exchange'] = False
|
||||||
|
# Update can_short flag
|
||||||
|
self._can_short = self.trading_mode != TradingMode.SPOT and strategy.can_short
|
||||||
|
|
||||||
self.strategy.ft_bot_start()
|
self.strategy.ft_bot_start()
|
||||||
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
|
|
||||||
|
|
||||||
def _load_protections(self, strategy: IStrategy):
|
def _load_protections(self, strategy: IStrategy):
|
||||||
if self.config.get('enable_protections', False):
|
if self.config.get('enable_protections', False):
|
||||||
@@ -740,7 +741,7 @@ class Backtesting:
|
|||||||
proposed_leverage=1.0,
|
proposed_leverage=1.0,
|
||||||
max_leverage=max_leverage,
|
max_leverage=max_leverage,
|
||||||
side=direction, entry_tag=entry_tag,
|
side=direction, entry_tag=entry_tag,
|
||||||
) if self._can_short else 1.0
|
) if self.trading_mode != TradingMode.SPOT else 1.0
|
||||||
# Cap leverage between 1.0 and max_leverage.
|
# Cap leverage between 1.0 and max_leverage.
|
||||||
leverage = min(max(leverage, 1.0), max_leverage)
|
leverage = min(max(leverage, 1.0), max_leverage)
|
||||||
|
|
||||||
@@ -1030,6 +1031,9 @@ class Backtesting:
|
|||||||
requested_stake=(
|
requested_stake=(
|
||||||
order.safe_remaining * order.ft_price / trade.leverage),
|
order.safe_remaining * order.ft_price / trade.leverage),
|
||||||
direction='short' if trade.is_short else 'long')
|
direction='short' if trade.is_short else 'long')
|
||||||
|
# Delete trade if no successful entries happened (if placing the new order failed)
|
||||||
|
if trade.open_order_id is None and trade.nr_of_successful_entries == 0:
|
||||||
|
return True
|
||||||
self.replaced_entry_orders += 1
|
self.replaced_entry_orders += 1
|
||||||
else:
|
else:
|
||||||
# assumption: there can't be multiple open entry orders at any given time
|
# assumption: there can't be multiple open entry orders at any given time
|
||||||
@@ -1155,6 +1159,8 @@ class Backtesting:
|
|||||||
while current_time <= end_date:
|
while current_time <= end_date:
|
||||||
open_trade_count_start = LocalTrade.bt_open_open_trade_count
|
open_trade_count_start = LocalTrade.bt_open_open_trade_count
|
||||||
self.check_abort()
|
self.check_abort()
|
||||||
|
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
|
||||||
|
current_time=current_time)
|
||||||
for i, pair in enumerate(data):
|
for i, pair in enumerate(data):
|
||||||
row_index = indexes[pair]
|
row_index = indexes[pair]
|
||||||
row = self.validate_row(data, pair, row_index, current_time)
|
row = self.validate_row(data, pair, row_index, current_time)
|
||||||
|
@@ -23,6 +23,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
NON_OPT_PARAM_APPENDIX = " # value loaded from strategy"
|
NON_OPT_PARAM_APPENDIX = " # value loaded from strategy"
|
||||||
|
|
||||||
|
HYPER_PARAMS_FILE_FORMAT = rapidjson.NM_NATIVE | rapidjson.NM_NAN
|
||||||
|
|
||||||
|
|
||||||
def hyperopt_serializer(x):
|
def hyperopt_serializer(x):
|
||||||
if isinstance(x, np.integer):
|
if isinstance(x, np.integer):
|
||||||
@@ -76,9 +78,18 @@ class HyperoptTools():
|
|||||||
with filename.open('w') as f:
|
with filename.open('w') as f:
|
||||||
rapidjson.dump(final_params, f, indent=2,
|
rapidjson.dump(final_params, f, indent=2,
|
||||||
default=hyperopt_serializer,
|
default=hyperopt_serializer,
|
||||||
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
|
number_mode=HYPER_PARAMS_FILE_FORMAT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_params(filename: Path) -> Dict:
|
||||||
|
"""
|
||||||
|
Load parameters from file
|
||||||
|
"""
|
||||||
|
with filename.open('r') as f:
|
||||||
|
params = rapidjson.load(f, number_mode=HYPER_PARAMS_FILE_FORMAT)
|
||||||
|
return params
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def try_export_params(config: Config, strategy_name: str, params: Dict):
|
def try_export_params(config: Config, strategy_name: str, params: Dict):
|
||||||
if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get('disableparamexport', False):
|
if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get('disableparamexport', False):
|
||||||
@@ -189,7 +200,7 @@ class HyperoptTools():
|
|||||||
for s in ['buy', 'sell', 'protection',
|
for s in ['buy', 'sell', 'protection',
|
||||||
'roi', 'stoploss', 'trailing', 'max_open_trades']:
|
'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=HYPER_PARAMS_FILE_FORMAT))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:",
|
HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:",
|
||||||
|
@@ -865,6 +865,11 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
|||||||
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
|
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
|
table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency)
|
||||||
|
if isinstance(table, str) and len(table) > 0:
|
||||||
|
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
|
||||||
|
print(table)
|
||||||
|
|
||||||
if (results.get('results_per_enter_tag') is not None
|
if (results.get('results_per_enter_tag') is not None
|
||||||
or results.get('results_per_buy_tag') is not None):
|
or results.get('results_per_buy_tag') is not None):
|
||||||
# results_per_buy_tag is deprecated and should be removed 2 versions after short golive.
|
# results_per_buy_tag is deprecated and should be removed 2 versions after short golive.
|
||||||
@@ -884,11 +889,6 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
|||||||
print(' EXIT REASON STATS '.center(len(table.splitlines()[0]), '='))
|
print(' EXIT REASON STATS '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency)
|
|
||||||
if isinstance(table, str) and len(table) > 0:
|
|
||||||
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
|
|
||||||
print(table)
|
|
||||||
|
|
||||||
for period in backtest_breakdown:
|
for period in backtest_breakdown:
|
||||||
days_breakdown_stats = generate_periodic_breakdown_stats(
|
days_breakdown_stats = generate_periodic_breakdown_stats(
|
||||||
trade_list=results['trades'], period=period)
|
trade_list=results['trades'], period=period)
|
||||||
@@ -917,11 +917,11 @@ def show_backtest_results(config: Config, backtest_stats: Dict):
|
|||||||
strategy, results, stake_currency,
|
strategy, results, stake_currency,
|
||||||
config.get('backtest_breakdown', []))
|
config.get('backtest_breakdown', []))
|
||||||
|
|
||||||
if len(backtest_stats['strategy']) > 1:
|
if len(backtest_stats['strategy']) > 0:
|
||||||
# Print Strategy summary table
|
# Print Strategy summary table
|
||||||
|
|
||||||
table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency)
|
table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency)
|
||||||
print(f"{results['backtest_start']} -> {results['backtest_end']} |"
|
print(f"Backtested {results['backtest_start']} -> {results['backtest_end']} |"
|
||||||
f" Max open trades : {results['max_open_trades']}")
|
f" Max open trades : {results['max_open_trades']}")
|
||||||
print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
|
print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
|
@@ -15,7 +15,8 @@ from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPE
|
|||||||
BuySell, LongShort)
|
BuySell, LongShort)
|
||||||
from freqtrade.enums import ExitType, TradingMode
|
from freqtrade.enums import ExitType, TradingMode
|
||||||
from freqtrade.exceptions import DependencyException, OperationalException
|
from freqtrade.exceptions import DependencyException, OperationalException
|
||||||
from freqtrade.exchange import amount_to_contract_precision, price_to_precision
|
from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision,
|
||||||
|
price_to_precision)
|
||||||
from freqtrade.leverage import interest
|
from freqtrade.leverage import interest
|
||||||
from freqtrade.persistence.base import ModelBase, SessionType
|
from freqtrade.persistence.base import ModelBase, SessionType
|
||||||
from freqtrade.util import FtPrecise
|
from freqtrade.util import FtPrecise
|
||||||
@@ -560,6 +561,9 @@ class LocalTrade():
|
|||||||
'trading_mode': self.trading_mode,
|
'trading_mode': self.trading_mode,
|
||||||
'funding_fees': self.funding_fees,
|
'funding_fees': self.funding_fees,
|
||||||
'open_order_id': self.open_order_id,
|
'open_order_id': self.open_order_id,
|
||||||
|
'amount_precision': self.amount_precision,
|
||||||
|
'price_precision': self.price_precision,
|
||||||
|
'precision_mode': self.precision_mode,
|
||||||
'orders': orders,
|
'orders': orders,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,7 +598,8 @@ class LocalTrade():
|
|||||||
"""
|
"""
|
||||||
Method used internally to set self.stop_loss.
|
Method used internally to set self.stop_loss.
|
||||||
"""
|
"""
|
||||||
stop_loss_norm = price_to_precision(stop_loss, self.price_precision, self.precision_mode)
|
stop_loss_norm = price_to_precision(stop_loss, self.price_precision, self.precision_mode,
|
||||||
|
rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP)
|
||||||
if not self.stop_loss:
|
if not self.stop_loss:
|
||||||
self.initial_stop_loss = stop_loss_norm
|
self.initial_stop_loss = stop_loss_norm
|
||||||
self.stop_loss = stop_loss_norm
|
self.stop_loss = stop_loss_norm
|
||||||
@@ -625,7 +630,8 @@ class LocalTrade():
|
|||||||
if self.initial_stop_loss_pct is None or refresh:
|
if self.initial_stop_loss_pct is None or refresh:
|
||||||
self.__set_stop_loss(new_loss, stoploss)
|
self.__set_stop_loss(new_loss, stoploss)
|
||||||
self.initial_stop_loss = price_to_precision(
|
self.initial_stop_loss = price_to_precision(
|
||||||
new_loss, self.price_precision, self.precision_mode)
|
new_loss, self.price_precision, self.precision_mode,
|
||||||
|
rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP)
|
||||||
self.initial_stop_loss_pct = -1 * abs(stoploss)
|
self.initial_stop_loss_pct = -1 * abs(stoploss)
|
||||||
|
|
||||||
# evaluate if the stop loss needs to be updated
|
# evaluate if the stop loss needs to be updated
|
||||||
@@ -689,21 +695,24 @@ class LocalTrade():
|
|||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'Got different open_order_id {self.open_order_id} != {order.order_id}')
|
f'Got different open_order_id {self.open_order_id} != {order.order_id}')
|
||||||
|
|
||||||
|
elif order.ft_order_side == 'stoploss' and order.status not in ('open', ):
|
||||||
|
self.stoploss_order_id = None
|
||||||
|
self.close_rate_requested = self.stop_loss
|
||||||
|
self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
|
||||||
|
if self.is_open:
|
||||||
|
logger.info(f'{order.order_type.upper()} is hit for {self}.')
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Unknown order type: {order.order_type}')
|
||||||
|
|
||||||
|
if order.ft_order_side != self.entry_side:
|
||||||
amount_tr = amount_to_contract_precision(self.amount, self.amount_precision,
|
amount_tr = amount_to_contract_precision(self.amount, self.amount_precision,
|
||||||
self.precision_mode, self.contract_size)
|
self.precision_mode, self.contract_size)
|
||||||
if isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC):
|
if isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC):
|
||||||
self.close(order.safe_price)
|
self.close(order.safe_price)
|
||||||
else:
|
else:
|
||||||
self.recalc_trade_from_orders()
|
self.recalc_trade_from_orders()
|
||||||
elif order.ft_order_side == 'stoploss' and order.status not in ('canceled', 'open'):
|
|
||||||
self.stoploss_order_id = None
|
|
||||||
self.close_rate_requested = self.stop_loss
|
|
||||||
self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
|
|
||||||
if self.is_open:
|
|
||||||
logger.info(f'{order.order_type.upper()} is hit for {self}.')
|
|
||||||
self.close(order.safe_price)
|
|
||||||
else:
|
|
||||||
raise ValueError(f'Unknown order type: {order.order_type}')
|
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
|
||||||
def close(self, rate: float, *, show_msg: bool = True) -> None:
|
def close(self, rate: float, *, show_msg: bool = True) -> None:
|
||||||
@@ -1660,8 +1669,10 @@ class Trade(ModelBase, LocalTrade):
|
|||||||
stop_loss=data["stop_loss_abs"],
|
stop_loss=data["stop_loss_abs"],
|
||||||
stop_loss_pct=data["stop_loss_ratio"],
|
stop_loss_pct=data["stop_loss_ratio"],
|
||||||
stoploss_order_id=data["stoploss_order_id"],
|
stoploss_order_id=data["stoploss_order_id"],
|
||||||
stoploss_last_update=(datetime.fromtimestamp(data["stoploss_last_update"] // 1000,
|
stoploss_last_update=(
|
||||||
tz=timezone.utc) if data["stoploss_last_update"] else None),
|
datetime.fromtimestamp(data["stoploss_last_update_timestamp"] // 1000,
|
||||||
|
tz=timezone.utc)
|
||||||
|
if data["stoploss_last_update_timestamp"] else None),
|
||||||
initial_stop_loss=data["initial_stop_loss_abs"],
|
initial_stop_loss=data["initial_stop_loss_abs"],
|
||||||
initial_stop_loss_pct=data["initial_stop_loss_ratio"],
|
initial_stop_loss_pct=data["initial_stop_loss_ratio"],
|
||||||
min_rate=data["min_rate"],
|
min_rate=data["min_rate"],
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
@@ -635,7 +636,7 @@ def load_and_plot_trades(config: Config):
|
|||||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
|
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
|
||||||
IStrategy.dp = DataProvider(config, exchange)
|
IStrategy.dp = DataProvider(config, exchange)
|
||||||
strategy.ft_bot_start()
|
strategy.ft_bot_start()
|
||||||
strategy.bot_loop_start()
|
strategy.bot_loop_start(datetime.now(timezone.utc))
|
||||||
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
|
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
|
||||||
timerange = plot_elements['timerange']
|
timerange = plot_elements['timerange']
|
||||||
trades = plot_elements['trades']
|
trades = plot_elements['trades']
|
||||||
|
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional
|
|||||||
|
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.exchange import ROUND_UP
|
||||||
from freqtrade.exchange.types import Ticker
|
from freqtrade.exchange.types import Ticker
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
@@ -61,9 +62,10 @@ class PrecisionFilter(IPairList):
|
|||||||
stop_price = ticker['last'] * self._stoploss
|
stop_price = ticker['last'] * self._stoploss
|
||||||
|
|
||||||
# Adjust stop-prices to precision
|
# Adjust stop-prices to precision
|
||||||
sp = self._exchange.price_to_precision(pair, stop_price)
|
sp = self._exchange.price_to_precision(pair, stop_price, rounding_mode=ROUND_UP)
|
||||||
|
|
||||||
stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99)
|
stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99,
|
||||||
|
rounding_mode=ROUND_UP)
|
||||||
logger.debug(f"{pair} - {sp} : {stop_gap_price}")
|
logger.debug(f"{pair} - {sp} : {stop_gap_price}")
|
||||||
|
|
||||||
if sp <= stop_gap_price:
|
if sp <= stop_gap_price:
|
||||||
|
@@ -276,6 +276,10 @@ class TradeSchema(BaseModel):
|
|||||||
funding_fees: Optional[float]
|
funding_fees: Optional[float]
|
||||||
trading_mode: Optional[TradingMode]
|
trading_mode: Optional[TradingMode]
|
||||||
|
|
||||||
|
amount_precision: Optional[float]
|
||||||
|
price_precision: Optional[float]
|
||||||
|
precision_mode: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
class OpenTradeSchema(TradeSchema):
|
class OpenTradeSchema(TradeSchema):
|
||||||
stoploss_current_dist: Optional[float]
|
stoploss_current_dist: Optional[float]
|
||||||
|
@@ -55,7 +55,7 @@ class UvicornServer(uvicorn.Server):
|
|||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def run_in_thread(self):
|
def run_in_thread(self):
|
||||||
self.thread = threading.Thread(target=self.run)
|
self.thread = threading.Thread(target=self.run, name='FTUvicorn')
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
while not self.started:
|
while not self.started:
|
||||||
time.sleep(1e-3)
|
time.sleep(1e-3)
|
||||||
|
@@ -13,6 +13,7 @@ from freqtrade.exceptions import OperationalException
|
|||||||
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
|
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
|
||||||
from freqtrade.rpc.api_server.ws.message_stream import MessageStream
|
from freqtrade.rpc.api_server.ws.message_stream import MessageStream
|
||||||
from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler
|
from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler
|
||||||
|
from freqtrade.rpc.rpc_types import RPCSendMsg
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -108,7 +109,7 @@ class ApiServer(RPCHandler):
|
|||||||
cls._has_rpc = False
|
cls._has_rpc = False
|
||||||
cls._rpc = None
|
cls._rpc = None
|
||||||
|
|
||||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
def send_msg(self, msg: RPCSendMsg) -> None:
|
||||||
"""
|
"""
|
||||||
Publish the message to the message stream
|
Publish the message to the message stream
|
||||||
"""
|
"""
|
||||||
|
@@ -30,6 +30,7 @@ from freqtrade.persistence import Order, PairLocks, Trade
|
|||||||
from freqtrade.persistence.models import PairLock
|
from freqtrade.persistence.models import PairLock
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||||
|
from freqtrade.rpc.rpc_types import RPCSendMsg
|
||||||
from freqtrade.wallets import PositionWallet, Wallet
|
from freqtrade.wallets import PositionWallet, Wallet
|
||||||
|
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ class RPCHandler:
|
|||||||
""" Cleanup pending module resources """
|
""" Cleanup pending module resources """
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def send_msg(self, msg: Dict[str, str]) -> None:
|
def send_msg(self, msg: RPCSendMsg) -> None:
|
||||||
""" Sends a message to all registered rpc modules """
|
""" Sends a message to all registered rpc modules """
|
||||||
|
|
||||||
|
|
||||||
|
@@ -3,11 +3,12 @@ This module contains class to manage RPC communications (Telegram, API, ...)
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from typing import Any, Dict, List
|
from typing import List
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.enums import NO_ECHO_MESSAGES, RPCMessageType
|
from freqtrade.enums import NO_ECHO_MESSAGES, RPCMessageType
|
||||||
from freqtrade.rpc import RPC, RPCHandler
|
from freqtrade.rpc import RPC, RPCHandler
|
||||||
|
from freqtrade.rpc.rpc_types import RPCSendMsg
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -58,7 +59,7 @@ class RPCManager:
|
|||||||
mod.cleanup()
|
mod.cleanup()
|
||||||
del mod
|
del mod
|
||||||
|
|
||||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
def send_msg(self, msg: RPCSendMsg) -> None:
|
||||||
"""
|
"""
|
||||||
Send given message to all registered rpc modules.
|
Send given message to all registered rpc modules.
|
||||||
A message consists of one or more key value pairs of strings.
|
A message consists of one or more key value pairs of strings.
|
||||||
@@ -69,10 +70,6 @@ class RPCManager:
|
|||||||
"""
|
"""
|
||||||
if msg.get('type') not in NO_ECHO_MESSAGES:
|
if msg.get('type') not in NO_ECHO_MESSAGES:
|
||||||
logger.info('Sending rpc message: %s', msg)
|
logger.info('Sending rpc message: %s', msg)
|
||||||
if 'pair' in msg:
|
|
||||||
msg.update({
|
|
||||||
'base_currency': self._rpc._freqtrade.exchange.get_pair_base_currency(msg['pair'])
|
|
||||||
})
|
|
||||||
for mod in self.registered_modules:
|
for mod in self.registered_modules:
|
||||||
logger.debug('Forwarding message to rpc.%s', mod.name)
|
logger.debug('Forwarding message to rpc.%s', mod.name)
|
||||||
try:
|
try:
|
||||||
|
128
freqtrade/rpc/rpc_types.py
Normal file
128
freqtrade/rpc/rpc_types.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, List, Literal, Optional, TypedDict, Union
|
||||||
|
|
||||||
|
from freqtrade.constants import PairWithTimeframe
|
||||||
|
from freqtrade.enums import RPCMessageType
|
||||||
|
|
||||||
|
|
||||||
|
class RPCSendMsgBase(TypedDict):
|
||||||
|
pass
|
||||||
|
# ty1pe: Literal[RPCMessageType]
|
||||||
|
|
||||||
|
|
||||||
|
class RPCStatusMsg(RPCSendMsgBase):
|
||||||
|
"""Used for Status, Startup and Warning messages"""
|
||||||
|
type: Literal[RPCMessageType.STATUS, RPCMessageType.STARTUP, RPCMessageType.WARNING]
|
||||||
|
status: str
|
||||||
|
|
||||||
|
|
||||||
|
class RPCStrategyMsg(RPCSendMsgBase):
|
||||||
|
"""Used for Status, Startup and Warning messages"""
|
||||||
|
type: Literal[RPCMessageType.STRATEGY_MSG]
|
||||||
|
msg: str
|
||||||
|
|
||||||
|
|
||||||
|
class RPCProtectionMsg(RPCSendMsgBase):
|
||||||
|
type: Literal[RPCMessageType.PROTECTION_TRIGGER, RPCMessageType.PROTECTION_TRIGGER_GLOBAL]
|
||||||
|
id: int
|
||||||
|
pair: str
|
||||||
|
base_currency: Optional[str]
|
||||||
|
lock_time: str
|
||||||
|
lock_timestamp: int
|
||||||
|
lock_end_time: str
|
||||||
|
lock_end_timestamp: int
|
||||||
|
reason: str
|
||||||
|
side: str
|
||||||
|
active: bool
|
||||||
|
|
||||||
|
|
||||||
|
class RPCWhitelistMsg(RPCSendMsgBase):
|
||||||
|
type: Literal[RPCMessageType.WHITELIST]
|
||||||
|
data: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class __RPCBuyMsgBase(RPCSendMsgBase):
|
||||||
|
trade_id: int
|
||||||
|
buy_tag: Optional[str]
|
||||||
|
enter_tag: Optional[str]
|
||||||
|
exchange: str
|
||||||
|
pair: str
|
||||||
|
base_currency: str
|
||||||
|
leverage: Optional[float]
|
||||||
|
direction: str
|
||||||
|
limit: float
|
||||||
|
open_rate: float
|
||||||
|
order_type: str
|
||||||
|
stake_amount: float
|
||||||
|
stake_currency: str
|
||||||
|
fiat_currency: Optional[str]
|
||||||
|
amount: float
|
||||||
|
open_date: datetime
|
||||||
|
current_rate: Optional[float]
|
||||||
|
sub_trade: bool
|
||||||
|
|
||||||
|
|
||||||
|
class RPCBuyMsg(__RPCBuyMsgBase):
|
||||||
|
type: Literal[RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL]
|
||||||
|
|
||||||
|
|
||||||
|
class RPCCancelMsg(__RPCBuyMsgBase):
|
||||||
|
type: Literal[RPCMessageType.ENTRY_CANCEL]
|
||||||
|
reason: str
|
||||||
|
|
||||||
|
|
||||||
|
class RPCSellMsg(__RPCBuyMsgBase):
|
||||||
|
type: Literal[RPCMessageType.EXIT, RPCMessageType.EXIT_FILL]
|
||||||
|
cumulative_profit: float
|
||||||
|
gain: str # Literal["profit", "loss"]
|
||||||
|
close_rate: float
|
||||||
|
profit_amount: float
|
||||||
|
profit_ratio: float
|
||||||
|
sell_reason: Optional[str]
|
||||||
|
exit_reason: Optional[str]
|
||||||
|
close_date: datetime
|
||||||
|
# current_rate: Optional[float]
|
||||||
|
order_rate: Optional[float]
|
||||||
|
|
||||||
|
|
||||||
|
class RPCSellCancelMsg(__RPCBuyMsgBase):
|
||||||
|
type: Literal[RPCMessageType.EXIT_CANCEL]
|
||||||
|
reason: str
|
||||||
|
gain: str # Literal["profit", "loss"]
|
||||||
|
profit_amount: float
|
||||||
|
profit_ratio: float
|
||||||
|
sell_reason: Optional[str]
|
||||||
|
exit_reason: Optional[str]
|
||||||
|
close_date: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class _AnalyzedDFData(TypedDict):
|
||||||
|
key: PairWithTimeframe
|
||||||
|
df: Any
|
||||||
|
la: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class RPCAnalyzedDFMsg(RPCSendMsgBase):
|
||||||
|
"""New Analyzed dataframe message"""
|
||||||
|
type: Literal[RPCMessageType.ANALYZED_DF]
|
||||||
|
data: _AnalyzedDFData
|
||||||
|
|
||||||
|
|
||||||
|
class RPCNewCandleMsg(RPCSendMsgBase):
|
||||||
|
"""New candle ping message, issued once per new candle/pair"""
|
||||||
|
type: Literal[RPCMessageType.NEW_CANDLE]
|
||||||
|
data: PairWithTimeframe
|
||||||
|
|
||||||
|
|
||||||
|
RPCSendMsg = Union[
|
||||||
|
RPCStatusMsg,
|
||||||
|
RPCStrategyMsg,
|
||||||
|
RPCProtectionMsg,
|
||||||
|
RPCWhitelistMsg,
|
||||||
|
RPCBuyMsg,
|
||||||
|
RPCCancelMsg,
|
||||||
|
RPCSellMsg,
|
||||||
|
RPCSellCancelMsg,
|
||||||
|
RPCAnalyzedDFMsg,
|
||||||
|
RPCNewCandleMsg
|
||||||
|
]
|
@@ -30,6 +30,7 @@ from freqtrade.exceptions import OperationalException
|
|||||||
from freqtrade.misc import chunks, plural, round_coin_value
|
from freqtrade.misc import chunks, plural, round_coin_value
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import RPC, RPCException, RPCHandler
|
from freqtrade.rpc import RPC, RPCException, RPCHandler
|
||||||
|
from freqtrade.rpc.rpc_types import RPCSendMsg
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -429,14 +430,14 @@ class Telegram(RPCHandler):
|
|||||||
return None
|
return None
|
||||||
return message
|
return message
|
||||||
|
|
||||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
def send_msg(self, msg: RPCSendMsg) -> None:
|
||||||
""" Send a message to telegram channel """
|
""" Send a message to telegram channel """
|
||||||
|
|
||||||
default_noti = 'on'
|
default_noti = 'on'
|
||||||
|
|
||||||
msg_type = msg['type']
|
msg_type = msg['type']
|
||||||
noti = ''
|
noti = ''
|
||||||
if msg_type == RPCMessageType.EXIT:
|
if msg['type'] == RPCMessageType.EXIT:
|
||||||
sell_noti = self._config['telegram'] \
|
sell_noti = self._config['telegram'] \
|
||||||
.get('notification_settings', {}).get(str(msg_type), {})
|
.get('notification_settings', {}).get(str(msg_type), {})
|
||||||
# For backward compatibility sell still can be string
|
# For backward compatibility sell still can be string
|
||||||
@@ -453,7 +454,7 @@ class Telegram(RPCHandler):
|
|||||||
# Notification disabled
|
# Notification disabled
|
||||||
return
|
return
|
||||||
|
|
||||||
message = self.compose_message(deepcopy(msg), msg_type)
|
message = self.compose_message(deepcopy(msg), msg_type) # type: ignore
|
||||||
if message:
|
if message:
|
||||||
self._send_msg(message, disable_notification=(noti == 'silent'))
|
self._send_msg(message, disable_notification=(noti == 'silent'))
|
||||||
|
|
||||||
|
@@ -10,6 +10,7 @@ from requests import RequestException, post
|
|||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.enums import RPCMessageType
|
from freqtrade.enums import RPCMessageType
|
||||||
from freqtrade.rpc import RPC, RPCHandler
|
from freqtrade.rpc import RPC, RPCHandler
|
||||||
|
from freqtrade.rpc.rpc_types import RPCSendMsg
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -41,7 +42,7 @@ class Webhook(RPCHandler):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _get_value_dict(self, msg: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
def _get_value_dict(self, msg: RPCSendMsg) -> Optional[Dict[str, Any]]:
|
||||||
whconfig = self._config['webhook']
|
whconfig = self._config['webhook']
|
||||||
# Deprecated 2022.10 - only keep generic method.
|
# Deprecated 2022.10 - only keep generic method.
|
||||||
if msg['type'] in [RPCMessageType.ENTRY]:
|
if msg['type'] in [RPCMessageType.ENTRY]:
|
||||||
@@ -75,7 +76,7 @@ class Webhook(RPCHandler):
|
|||||||
return None
|
return None
|
||||||
return valuedict
|
return valuedict
|
||||||
|
|
||||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
def send_msg(self, msg: RPCSendMsg) -> None:
|
||||||
""" Send a message to telegram channel """
|
""" Send a message to telegram channel """
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ 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
|
||||||
from freqtrade.misc import deep_merge_dicts, json_load
|
from freqtrade.misc import deep_merge_dicts
|
||||||
from freqtrade.optimize.hyperopt_tools import HyperoptTools
|
from freqtrade.optimize.hyperopt_tools import HyperoptTools
|
||||||
from freqtrade.strategy.parameters import BaseParameter
|
from freqtrade.strategy.parameters import BaseParameter
|
||||||
|
|
||||||
@@ -124,8 +124,7 @@ class HyperStrategyMixin:
|
|||||||
if filename.is_file():
|
if filename.is_file():
|
||||||
logger.info(f"Loading parameters from file {filename}")
|
logger.info(f"Loading parameters from file {filename}")
|
||||||
try:
|
try:
|
||||||
with filename.open('r') as f:
|
params = HyperoptTools.load_params(filename)
|
||||||
params = json_load(f)
|
|
||||||
if params.get('strategy_name') != self.__class__.__name__:
|
if params.get('strategy_name') != self.__class__.__name__:
|
||||||
raise OperationalException('Invalid parameter file provided.')
|
raise OperationalException('Invalid parameter file provided.')
|
||||||
return params
|
return params
|
||||||
|
@@ -251,11 +251,12 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def bot_loop_start(self, **kwargs) -> None:
|
def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
Called at the start of the bot iteration (one loop).
|
Called at the start of the bot iteration (one loop).
|
||||||
Might be used to perform pair-independent tasks
|
Might be used to perform pair-independent tasks
|
||||||
(e.g. gather some remote resource for comparison)
|
(e.g. gather some remote resource for comparison)
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
def bot_loop_start(self, **kwargs) -> None:
|
def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
Called at the start of the bot iteration (one loop).
|
Called at the start of the bot iteration (one loop).
|
||||||
Might be used to perform pair-independent tasks
|
Might be used to perform pair-independent tasks
|
||||||
@@ -8,6 +8,7 @@ def bot_loop_start(self, **kwargs) -> None:
|
|||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||||
|
|
||||||
When not implemented by a strategy, this simply does nothing.
|
When not implemented by a strategy, this simply does nothing.
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
@@ -7,9 +7,9 @@
|
|||||||
-r docs/requirements-docs.txt
|
-r docs/requirements-docs.txt
|
||||||
|
|
||||||
coveralls==3.3.1
|
coveralls==3.3.1
|
||||||
ruff==0.0.257
|
ruff==0.0.260
|
||||||
mypy==1.1.1
|
mypy==1.1.1
|
||||||
pre-commit==3.2.0
|
pre-commit==3.2.1
|
||||||
pytest==7.2.2
|
pytest==7.2.2
|
||||||
pytest-asyncio==0.21.0
|
pytest-asyncio==0.21.0
|
||||||
pytest-cov==4.0.0
|
pytest-cov==4.0.0
|
||||||
@@ -25,8 +25,8 @@ httpx==0.23.3
|
|||||||
nbconvert==7.2.10
|
nbconvert==7.2.10
|
||||||
|
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==5.3.0.4
|
types-cachetools==5.3.0.5
|
||||||
types-filelock==3.2.7
|
types-filelock==3.2.7
|
||||||
types-requests==2.28.11.15
|
types-requests==2.28.11.17
|
||||||
types-tabulate==0.9.0.1
|
types-tabulate==0.9.0.2
|
||||||
types-python-dateutil==2.8.19.10
|
types-python-dateutil==2.8.19.11
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
# Required for freqai
|
# Required for freqai
|
||||||
scikit-learn==1.1.3
|
scikit-learn==1.1.3
|
||||||
joblib==1.2.0
|
joblib==1.2.0
|
||||||
catboost==1.1.1; platform_machine != 'aarch64' and python_version < '3.11'
|
catboost==1.1.1; platform_machine != 'aarch64' and 'arm' not in platform_machine and python_version < '3.11'
|
||||||
lightgbm==3.3.5
|
lightgbm==3.3.5
|
||||||
xgboost==1.7.4
|
xgboost==1.7.5
|
||||||
tensorboard==2.12.0
|
tensorboard==2.12.1
|
||||||
|
@@ -5,5 +5,5 @@
|
|||||||
scipy==1.10.1
|
scipy==1.10.1
|
||||||
scikit-learn==1.1.3
|
scikit-learn==1.1.3
|
||||||
scikit-optimize==0.9.0
|
scikit-optimize==0.9.0
|
||||||
filelock==3.10.0
|
filelock==3.10.6
|
||||||
progressbar2==4.2.0
|
progressbar2==4.2.0
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
# Include all requirements to run the bot.
|
# Include all requirements to run the bot.
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
plotly==5.13.1
|
plotly==5.14.0
|
||||||
|
@@ -2,17 +2,17 @@ numpy==1.24.2
|
|||||||
pandas==1.5.3
|
pandas==1.5.3
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==3.0.23
|
ccxt==3.0.50
|
||||||
cryptography==39.0.2
|
cryptography==40.0.1
|
||||||
aiohttp==3.8.4
|
aiohttp==3.8.4
|
||||||
SQLAlchemy==2.0.7
|
SQLAlchemy==2.0.8
|
||||||
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.2
|
requests==2.28.2
|
||||||
urllib3==1.26.15
|
urllib3==1.26.15
|
||||||
jsonschema==4.17.3
|
jsonschema==4.17.3
|
||||||
TA-Lib==0.4.25
|
TA-Lib==0.4.26
|
||||||
technical==1.4.0
|
technical==1.4.0
|
||||||
tabulate==0.9.0
|
tabulate==0.9.0
|
||||||
pycoingecko==3.1.0
|
pycoingecko==3.1.0
|
||||||
@@ -28,14 +28,14 @@ py_find_1st==1.1.5
|
|||||||
# Load ticker files 30% faster
|
# Load ticker files 30% faster
|
||||||
python-rapidjson==1.10
|
python-rapidjson==1.10
|
||||||
# Properly format api responses
|
# Properly format api responses
|
||||||
orjson==3.8.7
|
orjson==3.8.9
|
||||||
|
|
||||||
# Notify systemd
|
# Notify systemd
|
||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.95.0
|
fastapi==0.95.0
|
||||||
pydantic==1.10.6
|
pydantic==1.10.7
|
||||||
uvicorn==0.21.1
|
uvicorn==0.21.1
|
||||||
pyjwt==2.6.0
|
pyjwt==2.6.0
|
||||||
aiofiles==23.1.0
|
aiofiles==23.1.0
|
||||||
@@ -53,7 +53,7 @@ python-dateutil==2.8.2
|
|||||||
schedule==1.1.0
|
schedule==1.1.0
|
||||||
|
|
||||||
#WS Messages
|
#WS Messages
|
||||||
websockets==10.4
|
websockets==11.0
|
||||||
janus==1.0.0
|
janus==1.0.0
|
||||||
|
|
||||||
ast-comments==1.0.1
|
ast-comments==1.0.1
|
||||||
|
2
setup.py
2
setup.py
@@ -59,7 +59,7 @@ setup(
|
|||||||
install_requires=[
|
install_requires=[
|
||||||
# from requirements.txt
|
# from requirements.txt
|
||||||
'ccxt>=2.6.26',
|
'ccxt>=2.6.26',
|
||||||
'SQLAlchemy',
|
'SQLAlchemy>=2.0.6',
|
||||||
'python-telegram-bot>=13.4',
|
'python-telegram-bot>=13.4',
|
||||||
'arrow>=0.17.0',
|
'arrow>=0.17.0',
|
||||||
'cachetools',
|
'cachetools',
|
||||||
|
@@ -252,7 +252,7 @@ def test_datahandler__check_empty_df(testdatadir, caplog):
|
|||||||
assert log_has_re(expected_text, caplog)
|
assert log_has_re(expected_text, caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('datahandler', ['feather', 'parquet'])
|
@pytest.mark.parametrize('datahandler', ['parquet'])
|
||||||
def test_datahandler_trades_not_supported(datahandler, testdatadir, ):
|
def test_datahandler_trades_not_supported(datahandler, testdatadir, ):
|
||||||
dh = get_datahandler(testdatadir, datahandler)
|
dh = get_datahandler(testdatadir, datahandler)
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
@@ -496,6 +496,58 @@ def test_hdf5datahandler_ohlcv_purge(mocker, testdatadir):
|
|||||||
assert unlinkmock.call_count == 2
|
assert unlinkmock.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_featherdatahandler_trades_load(testdatadir):
|
||||||
|
dh = get_datahandler(testdatadir, 'feather')
|
||||||
|
trades = dh.trades_load('XRP/ETH')
|
||||||
|
assert isinstance(trades, list)
|
||||||
|
assert trades[0][0] == 1570752011620
|
||||||
|
assert trades[-1][-1] == 0.1986231
|
||||||
|
|
||||||
|
trades1 = dh.trades_load('UNITTEST/NONEXIST')
|
||||||
|
assert trades1 == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_featherdatahandler_trades_store(testdatadir, tmpdir):
|
||||||
|
tmpdir1 = Path(tmpdir)
|
||||||
|
dh = get_datahandler(testdatadir, 'feather')
|
||||||
|
trades = dh.trades_load('XRP/ETH')
|
||||||
|
|
||||||
|
dh1 = get_datahandler(tmpdir1, 'feather')
|
||||||
|
dh1.trades_store('XRP/NEW', trades)
|
||||||
|
file = tmpdir1 / 'XRP_NEW-trades.feather'
|
||||||
|
assert file.is_file()
|
||||||
|
# Load trades back
|
||||||
|
trades_new = dh1.trades_load('XRP/NEW')
|
||||||
|
|
||||||
|
assert len(trades_new) == len(trades)
|
||||||
|
assert trades[0][0] == trades_new[0][0]
|
||||||
|
assert trades[0][1] == trades_new[0][1]
|
||||||
|
# assert trades[0][2] == trades_new[0][2] # This is nan - so comparison does not make sense
|
||||||
|
assert trades[0][3] == trades_new[0][3]
|
||||||
|
assert trades[0][4] == trades_new[0][4]
|
||||||
|
assert trades[0][5] == trades_new[0][5]
|
||||||
|
assert trades[0][6] == trades_new[0][6]
|
||||||
|
assert trades[-1][0] == trades_new[-1][0]
|
||||||
|
assert trades[-1][1] == trades_new[-1][1]
|
||||||
|
# assert trades[-1][2] == trades_new[-1][2] # This is nan - so comparison does not make sense
|
||||||
|
assert trades[-1][3] == trades_new[-1][3]
|
||||||
|
assert trades[-1][4] == trades_new[-1][4]
|
||||||
|
assert trades[-1][5] == trades_new[-1][5]
|
||||||
|
assert trades[-1][6] == trades_new[-1][6]
|
||||||
|
|
||||||
|
|
||||||
|
def test_featherdatahandler_trades_purge(mocker, testdatadir):
|
||||||
|
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
|
||||||
|
unlinkmock = mocker.patch.object(Path, "unlink", MagicMock())
|
||||||
|
dh = get_datahandler(testdatadir, 'feather')
|
||||||
|
assert not dh.trades_purge('UNITTEST/NONEXIST')
|
||||||
|
assert unlinkmock.call_count == 0
|
||||||
|
|
||||||
|
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||||
|
assert dh.trades_purge('UNITTEST/NONEXIST')
|
||||||
|
assert unlinkmock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_gethandlerclass():
|
def test_gethandlerclass():
|
||||||
cl = get_datahandlerclass('json')
|
cl = get_datahandlerclass('json')
|
||||||
assert cl == JsonDataHandler
|
assert cl == JsonDataHandler
|
||||||
|
@@ -15,8 +15,8 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
|||||||
('buy', 'limit', 'gtc', {'timeInForce': 'GTC'}),
|
('buy', 'limit', 'gtc', {'timeInForce': 'GTC'}),
|
||||||
('buy', 'limit', 'IOC', {'timeInForce': 'IOC'}),
|
('buy', 'limit', 'IOC', {'timeInForce': 'IOC'}),
|
||||||
('buy', 'market', 'IOC', {}),
|
('buy', 'market', 'IOC', {}),
|
||||||
('buy', 'limit', 'PO', {'postOnly': True}),
|
('buy', 'limit', 'PO', {'timeInForce': 'PO'}),
|
||||||
('sell', 'limit', 'PO', {'postOnly': True}),
|
('sell', 'limit', 'PO', {'timeInForce': 'PO'}),
|
||||||
('sell', 'market', 'PO', {}),
|
('sell', 'market', 'PO', {}),
|
||||||
])
|
])
|
||||||
def test__get_params_binance(default_conf, mocker, side, type, time_in_force, expected):
|
def test__get_params_binance(default_conf, mocker, side, type, time_in_force, expected):
|
||||||
@@ -48,7 +48,7 @@ def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expecte
|
|||||||
default_conf['margin_mode'] = MarginMode.ISOLATED
|
default_conf['margin_mode'] = MarginMode.ISOLATED
|
||||||
default_conf['trading_mode'] = trademode
|
default_conf['trading_mode'] = trademode
|
||||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ def test_create_stoploss_order_dry_run_binance(default_conf, mocker):
|
|||||||
order_type = 'stop_loss_limit'
|
order_type = 'stop_loss_limit'
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
|||||||
import arrow
|
import arrow
|
||||||
import ccxt
|
import ccxt
|
||||||
import pytest
|
import pytest
|
||||||
|
from ccxt import DECIMAL_PLACES, ROUND, ROUND_UP, TICK_SIZE, TRUNCATE
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||||
@@ -113,18 +114,21 @@ async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fu
|
|||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
await getattr(exchange, fun)(**kwargs)
|
await getattr(exchange, fun)(**kwargs)
|
||||||
assert api_mock.__dict__[mock_ccxt_fun].call_count == retries
|
assert api_mock.__dict__[mock_ccxt_fun].call_count == retries
|
||||||
|
exchange.close()
|
||||||
|
|
||||||
with pytest.raises(TemporaryError):
|
with pytest.raises(TemporaryError):
|
||||||
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef"))
|
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
await getattr(exchange, fun)(**kwargs)
|
await getattr(exchange, fun)(**kwargs)
|
||||||
assert api_mock.__dict__[mock_ccxt_fun].call_count == retries
|
assert api_mock.__dict__[mock_ccxt_fun].call_count == retries
|
||||||
|
exchange.close()
|
||||||
|
|
||||||
with pytest.raises(OperationalException):
|
with pytest.raises(OperationalException):
|
||||||
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
|
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
await getattr(exchange, fun)(**kwargs)
|
await getattr(exchange, fun)(**kwargs)
|
||||||
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
|
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
|
||||||
|
exchange.close()
|
||||||
|
|
||||||
|
|
||||||
def test_init(default_conf, mocker, caplog):
|
def test_init(default_conf, mocker, caplog):
|
||||||
@@ -312,35 +316,54 @@ def test_amount_to_precision(amount, precision_mode, precision, expected,):
|
|||||||
assert amount_to_precision(amount, precision, precision_mode) == expected
|
assert amount_to_precision(amount, precision, precision_mode) == expected
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("price,precision_mode,precision,expected", [
|
@pytest.mark.parametrize("price,precision_mode,precision,expected,rounding_mode", [
|
||||||
(2.34559, 2, 4, 2.3456),
|
# Tests for DECIMAL_PLACES, ROUND_UP
|
||||||
(2.34559, 2, 5, 2.34559),
|
(2.34559, 2, 4, 2.3456, ROUND_UP),
|
||||||
(2.34559, 2, 3, 2.346),
|
(2.34559, 2, 5, 2.34559, ROUND_UP),
|
||||||
(2.9999, 2, 3, 3.000),
|
(2.34559, 2, 3, 2.346, ROUND_UP),
|
||||||
(2.9909, 2, 3, 2.991),
|
(2.9999, 2, 3, 3.000, ROUND_UP),
|
||||||
# Tests for Tick_size
|
(2.9909, 2, 3, 2.991, ROUND_UP),
|
||||||
(2.34559, 4, 0.0001, 2.3456),
|
# Tests for DECIMAL_PLACES, ROUND
|
||||||
(2.34559, 4, 0.00001, 2.34559),
|
(2.345600000000001, DECIMAL_PLACES, 4, 2.3456, ROUND),
|
||||||
(2.34559, 4, 0.001, 2.346),
|
(2.345551, DECIMAL_PLACES, 4, 2.3456, ROUND),
|
||||||
(2.9999, 4, 0.001, 3.000),
|
(2.49, DECIMAL_PLACES, 0, 2., ROUND),
|
||||||
(2.9909, 4, 0.001, 2.991),
|
(2.51, DECIMAL_PLACES, 0, 3., ROUND),
|
||||||
(2.9909, 4, 0.005, 2.995),
|
(5.1, DECIMAL_PLACES, -1, 10., ROUND),
|
||||||
(2.9973, 4, 0.005, 3.0),
|
(4.9, DECIMAL_PLACES, -1, 0., ROUND),
|
||||||
(2.9977, 4, 0.005, 3.0),
|
# Tests for TICK_SIZE, ROUND_UP
|
||||||
(234.43, 4, 0.5, 234.5),
|
(2.34559, TICK_SIZE, 0.0001, 2.3456, ROUND_UP),
|
||||||
(234.53, 4, 0.5, 235.0),
|
(2.34559, TICK_SIZE, 0.00001, 2.34559, ROUND_UP),
|
||||||
(0.891534, 4, 0.0001, 0.8916),
|
(2.34559, TICK_SIZE, 0.001, 2.346, ROUND_UP),
|
||||||
(64968.89, 4, 0.01, 64968.89),
|
(2.9999, TICK_SIZE, 0.001, 3.000, ROUND_UP),
|
||||||
(0.000000003483, 4, 1e-12, 0.000000003483),
|
(2.9909, TICK_SIZE, 0.001, 2.991, ROUND_UP),
|
||||||
|
(2.9909, TICK_SIZE, 0.005, 2.995, ROUND_UP),
|
||||||
|
(2.9973, TICK_SIZE, 0.005, 3.0, ROUND_UP),
|
||||||
|
(2.9977, TICK_SIZE, 0.005, 3.0, ROUND_UP),
|
||||||
|
(234.43, TICK_SIZE, 0.5, 234.5, ROUND_UP),
|
||||||
|
(234.53, TICK_SIZE, 0.5, 235.0, ROUND_UP),
|
||||||
|
(0.891534, TICK_SIZE, 0.0001, 0.8916, ROUND_UP),
|
||||||
|
(64968.89, TICK_SIZE, 0.01, 64968.89, ROUND_UP),
|
||||||
|
(0.000000003483, TICK_SIZE, 1e-12, 0.000000003483, ROUND_UP),
|
||||||
|
# Tests for TICK_SIZE, ROUND
|
||||||
|
(2.49, TICK_SIZE, 1., 2., ROUND),
|
||||||
|
(2.51, TICK_SIZE, 1., 3., ROUND),
|
||||||
|
(2.000000051, TICK_SIZE, 0.0000001, 2.0000001, ROUND),
|
||||||
|
(2.000000049, TICK_SIZE, 0.0000001, 2., ROUND),
|
||||||
|
(2.9909, TICK_SIZE, 0.005, 2.990, ROUND),
|
||||||
|
(2.9973, TICK_SIZE, 0.005, 2.995, ROUND),
|
||||||
|
(2.9977, TICK_SIZE, 0.005, 3.0, ROUND),
|
||||||
|
(234.24, TICK_SIZE, 0.5, 234., ROUND),
|
||||||
|
(234.26, TICK_SIZE, 0.5, 234.5, ROUND),
|
||||||
|
# Tests for TRUNCATTE
|
||||||
|
(2.34559, 2, 4, 2.3455, TRUNCATE),
|
||||||
|
(2.34559, 2, 5, 2.34559, TRUNCATE),
|
||||||
|
(2.34559, 2, 3, 2.345, TRUNCATE),
|
||||||
|
(2.9999, 2, 3, 2.999, TRUNCATE),
|
||||||
|
(2.9909, 2, 3, 2.990, TRUNCATE),
|
||||||
])
|
])
|
||||||
def test_price_to_precision(price, precision_mode, precision, expected):
|
def test_price_to_precision(price, precision_mode, precision, expected, rounding_mode):
|
||||||
# digits counting mode
|
assert price_to_precision(
|
||||||
# DECIMAL_PLACES = 2
|
price, precision, precision_mode, rounding_mode=rounding_mode) == expected
|
||||||
# SIGNIFICANT_DIGITS = 3
|
|
||||||
# TICK_SIZE = 4
|
|
||||||
|
|
||||||
assert price_to_precision(price, precision, precision_mode) == expected
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("price,precision_mode,precision,expected", [
|
@pytest.mark.parametrize("price,precision_mode,precision,expected", [
|
||||||
@@ -414,7 +437,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None:
|
|||||||
}
|
}
|
||||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
||||||
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
|
||||||
expected_result = 2 * 2 * (1 + 0.05) / (1 - abs(stoploss))
|
expected_result = 2 * 2 * (1 + 0.05)
|
||||||
assert pytest.approx(result) == expected_result
|
assert pytest.approx(result) == expected_result
|
||||||
# With Leverage
|
# With Leverage
|
||||||
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0)
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0)
|
||||||
@@ -423,14 +446,14 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None:
|
|||||||
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2)
|
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2)
|
||||||
assert result == 20000
|
assert result == 20000
|
||||||
|
|
||||||
# min amount and cost are set (cost is minimal)
|
# min amount and cost are set (cost is minimal and therefore ignored)
|
||||||
markets["ETH/BTC"]["limits"] = {
|
markets["ETH/BTC"]["limits"] = {
|
||||||
'cost': {'min': 2, 'max': None},
|
'cost': {'min': 2, 'max': None},
|
||||||
'amount': {'min': 2, 'max': None},
|
'amount': {'min': 2, 'max': None},
|
||||||
}
|
}
|
||||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
||||||
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
|
||||||
expected_result = max(2, 2 * 2) * (1 + 0.05) / (1 - abs(stoploss))
|
expected_result = max(2, 2 * 2) * (1 + 0.05)
|
||||||
assert pytest.approx(result) == expected_result
|
assert pytest.approx(result) == expected_result
|
||||||
# With Leverage
|
# With Leverage
|
||||||
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10)
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10)
|
||||||
@@ -473,6 +496,9 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None:
|
|||||||
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2)
|
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2)
|
||||||
assert result == 1000
|
assert result == 1000
|
||||||
|
|
||||||
|
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2, 12.0)
|
||||||
|
assert result == 1000 / 12
|
||||||
|
|
||||||
markets["ETH/BTC"]["contractSize"] = '0.01'
|
markets["ETH/BTC"]["contractSize"] = '0.01'
|
||||||
default_conf['trading_mode'] = 'futures'
|
default_conf['trading_mode'] = 'futures'
|
||||||
default_conf['margin_mode'] = 'isolated'
|
default_conf['margin_mode'] = 'isolated'
|
||||||
@@ -1436,7 +1462,10 @@ def test_buy_prod(default_conf, mocker, exchange_name):
|
|||||||
assert api_mock.create_order.call_args[0][1] == order_type
|
assert api_mock.create_order.call_args[0][1] == order_type
|
||||||
assert api_mock.create_order.call_args[0][2] == 'buy'
|
assert api_mock.create_order.call_args[0][2] == 'buy'
|
||||||
assert api_mock.create_order.call_args[0][3] == 1
|
assert api_mock.create_order.call_args[0][3] == 1
|
||||||
assert api_mock.create_order.call_args[0][4] is None
|
if exchange._order_needs_price(order_type):
|
||||||
|
assert api_mock.create_order.call_args[0][4] == 200
|
||||||
|
else:
|
||||||
|
assert api_mock.create_order.call_args[0][4] is None
|
||||||
|
|
||||||
api_mock.create_order.reset_mock()
|
api_mock.create_order.reset_mock()
|
||||||
order_type = 'limit'
|
order_type = 'limit'
|
||||||
@@ -1541,7 +1570,10 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name):
|
|||||||
assert api_mock.create_order.call_args[0][1] == order_type
|
assert api_mock.create_order.call_args[0][1] == order_type
|
||||||
assert api_mock.create_order.call_args[0][2] == 'buy'
|
assert api_mock.create_order.call_args[0][2] == 'buy'
|
||||||
assert api_mock.create_order.call_args[0][3] == 1
|
assert api_mock.create_order.call_args[0][3] == 1
|
||||||
assert api_mock.create_order.call_args[0][4] is None
|
if exchange._order_needs_price(order_type):
|
||||||
|
assert api_mock.create_order.call_args[0][4] == 200
|
||||||
|
else:
|
||||||
|
assert api_mock.create_order.call_args[0][4] is None
|
||||||
# Market orders should not send timeInForce!!
|
# Market orders should not send timeInForce!!
|
||||||
assert "timeInForce" not in api_mock.create_order.call_args[0][5]
|
assert "timeInForce" not in api_mock.create_order.call_args[0][5]
|
||||||
|
|
||||||
@@ -1585,7 +1617,10 @@ def test_sell_prod(default_conf, mocker, exchange_name):
|
|||||||
assert api_mock.create_order.call_args[0][1] == order_type
|
assert api_mock.create_order.call_args[0][1] == order_type
|
||||||
assert api_mock.create_order.call_args[0][2] == 'sell'
|
assert api_mock.create_order.call_args[0][2] == 'sell'
|
||||||
assert api_mock.create_order.call_args[0][3] == 1
|
assert api_mock.create_order.call_args[0][3] == 1
|
||||||
assert api_mock.create_order.call_args[0][4] is None
|
if exchange._order_needs_price(order_type):
|
||||||
|
assert api_mock.create_order.call_args[0][4] == 200
|
||||||
|
else:
|
||||||
|
assert api_mock.create_order.call_args[0][4] is None
|
||||||
|
|
||||||
api_mock.create_order.reset_mock()
|
api_mock.create_order.reset_mock()
|
||||||
order_type = 'limit'
|
order_type = 'limit'
|
||||||
@@ -1679,7 +1714,10 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name):
|
|||||||
assert api_mock.create_order.call_args[0][1] == order_type
|
assert api_mock.create_order.call_args[0][1] == order_type
|
||||||
assert api_mock.create_order.call_args[0][2] == 'sell'
|
assert api_mock.create_order.call_args[0][2] == 'sell'
|
||||||
assert api_mock.create_order.call_args[0][3] == 1
|
assert api_mock.create_order.call_args[0][3] == 1
|
||||||
assert api_mock.create_order.call_args[0][4] is None
|
if exchange._order_needs_price(order_type):
|
||||||
|
assert api_mock.create_order.call_args[0][4] == 200
|
||||||
|
else:
|
||||||
|
assert api_mock.create_order.call_args[0][4] is None
|
||||||
# Market orders should not send timeInForce!!
|
# Market orders should not send timeInForce!!
|
||||||
assert "timeInForce" not in api_mock.create_order.call_args[0][5]
|
assert "timeInForce" not in api_mock.create_order.call_args[0][5]
|
||||||
|
|
||||||
@@ -2248,7 +2286,6 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach
|
|||||||
assert res[pair2].at[0, 'open']
|
assert res[pair2].at[0, 'open']
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name):
|
async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name):
|
||||||
ohlcv = [
|
ohlcv = [
|
||||||
@@ -2277,7 +2314,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
|
|||||||
assert res[3] == ohlcv
|
assert res[3] == ohlcv
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
||||||
assert not log_has(f"Using cached candle (OHLCV) data for {pair} ...", caplog)
|
assert not log_has(f"Using cached candle (OHLCV) data for {pair} ...", caplog)
|
||||||
|
exchange.close()
|
||||||
# exchange = Exchange(default_conf)
|
# exchange = Exchange(default_conf)
|
||||||
await async_ccxt_exception(mocker, default_conf, MagicMock(),
|
await async_ccxt_exception(mocker, default_conf, MagicMock(),
|
||||||
"_async_get_candle_history", "fetch_ohlcv",
|
"_async_get_candle_history", "fetch_ohlcv",
|
||||||
@@ -2292,15 +2329,17 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
|
|||||||
await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT,
|
await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT,
|
||||||
(arrow.utcnow().int_timestamp - 2000) * 1000)
|
(arrow.utcnow().int_timestamp - 2000) * 1000)
|
||||||
|
|
||||||
|
exchange.close()
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching '
|
with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching '
|
||||||
r'historical candle \(OHLCV\) data\..*'):
|
r'historical candle \(OHLCV\) data\..*'):
|
||||||
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported("Not supported"))
|
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported("Not supported"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT,
|
await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT,
|
||||||
(arrow.utcnow().int_timestamp - 2000) * 1000)
|
(arrow.utcnow().int_timestamp - 2000) * 1000)
|
||||||
|
exchange.close()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog):
|
async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog):
|
||||||
from freqtrade.exchange.common import _reset_logging_mixin
|
from freqtrade.exchange.common import _reset_logging_mixin
|
||||||
_reset_logging_mixin()
|
_reset_logging_mixin()
|
||||||
@@ -2341,9 +2380,9 @@ async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog):
|
|||||||
# Expect the "returned exception" message 12 times (4 retries * 3 (loop))
|
# Expect the "returned exception" message 12 times (4 retries * 3 (loop))
|
||||||
assert num_log_has_re(msg, caplog) == 12
|
assert num_log_has_re(msg, caplog) == 12
|
||||||
assert num_log_has_re(msg2, caplog) == 9
|
assert num_log_has_re(msg2, caplog) == 9
|
||||||
|
exchange.close()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test__async_get_candle_history_empty(default_conf, mocker, caplog):
|
async def test__async_get_candle_history_empty(default_conf, mocker, caplog):
|
||||||
""" Test empty exchange result """
|
""" Test empty exchange result """
|
||||||
ohlcv = []
|
ohlcv = []
|
||||||
@@ -2363,6 +2402,7 @@ async def test__async_get_candle_history_empty(default_conf, mocker, caplog):
|
|||||||
assert res[2] == CandleType.SPOT
|
assert res[2] == CandleType.SPOT
|
||||||
assert res[3] == ohlcv
|
assert res[3] == ohlcv
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
||||||
|
exchange.close()
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog):
|
def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog):
|
||||||
@@ -2757,7 +2797,6 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na
|
|||||||
assert res_ohlcv[9][5] == 2.31452783
|
assert res_ohlcv[9][5] == 2.31452783
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name,
|
async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name,
|
||||||
fetch_trades_result):
|
fetch_trades_result):
|
||||||
@@ -2785,8 +2824,8 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name,
|
|||||||
assert exchange._api_async.fetch_trades.call_args[1]['limit'] == 1000
|
assert exchange._api_async.fetch_trades.call_args[1]['limit'] == 1000
|
||||||
assert exchange._api_async.fetch_trades.call_args[1]['params'] == {'from': '123'}
|
assert exchange._api_async.fetch_trades.call_args[1]['params'] == {'from': '123'}
|
||||||
assert log_has_re(f"Fetching trades for pair {pair}, params: .*", caplog)
|
assert log_has_re(f"Fetching trades for pair {pair}, params: .*", caplog)
|
||||||
|
exchange.close()
|
||||||
|
|
||||||
exchange = Exchange(default_conf)
|
|
||||||
await async_ccxt_exception(mocker, default_conf, MagicMock(),
|
await async_ccxt_exception(mocker, default_conf, MagicMock(),
|
||||||
"_async_fetch_trades", "fetch_trades",
|
"_async_fetch_trades", "fetch_trades",
|
||||||
pair='ABCD/BTC', since=None)
|
pair='ABCD/BTC', since=None)
|
||||||
@@ -2796,15 +2835,16 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name,
|
|||||||
api_mock.fetch_trades = MagicMock(side_effect=ccxt.BaseError("Unknown error"))
|
api_mock.fetch_trades = MagicMock(side_effect=ccxt.BaseError("Unknown error"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000)
|
await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000)
|
||||||
|
exchange.close()
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching '
|
with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching '
|
||||||
r'historical trade data\..*'):
|
r'historical trade data\..*'):
|
||||||
api_mock.fetch_trades = MagicMock(side_effect=ccxt.NotSupported("Not supported"))
|
api_mock.fetch_trades = MagicMock(side_effect=ccxt.NotSupported("Not supported"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000)
|
await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000)
|
||||||
|
exchange.close()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
async def test__async_fetch_trades_contract_size(default_conf, mocker, caplog, exchange_name,
|
async def test__async_fetch_trades_contract_size(default_conf, mocker, caplog, exchange_name,
|
||||||
fetch_trades_result):
|
fetch_trades_result):
|
||||||
@@ -2839,6 +2879,7 @@ async def test__async_fetch_trades_contract_size(default_conf, mocker, caplog, e
|
|||||||
pair = 'ETH/USDT:USDT'
|
pair = 'ETH/USDT:USDT'
|
||||||
res = await exchange._async_fetch_trades(pair, since=None, params=None)
|
res = await exchange._async_fetch_trades(pair, since=None, params=None)
|
||||||
assert res[0][5] == 300
|
assert res[0][5] == 300
|
||||||
|
exchange.close()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -4807,7 +4848,6 @@ def test_load_leverage_tiers(mocker, default_conf, leverage_tiers, exchange_name
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
@pytest.mark.parametrize('exchange_name', EXCHANGES)
|
@pytest.mark.parametrize('exchange_name', EXCHANGES)
|
||||||
async def test_get_market_leverage_tiers(mocker, default_conf, exchange_name):
|
async def test_get_market_leverage_tiers(mocker, default_conf, exchange_name):
|
||||||
default_conf['exchange']['name'] = exchange_name
|
default_conf['exchange']['name'] = exchange_name
|
||||||
@@ -5264,7 +5304,7 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun
|
|||||||
})
|
})
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
exchange.get_contract_size = MagicMock(return_value=contract_size)
|
exchange.get_contract_size = MagicMock(return_value=contract_size)
|
||||||
@@ -5284,3 +5324,10 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun
|
|||||||
assert order['cost'] == 100
|
assert order['cost'] == 100
|
||||||
assert order['filled'] == 100
|
assert order['filled'] == 100
|
||||||
assert order['remaining'] == 100
|
assert order['remaining'] == 100
|
||||||
|
|
||||||
|
|
||||||
|
def test_price_to_precision_with_default_conf(default_conf, mocker):
|
||||||
|
conf = copy.deepcopy(default_conf)
|
||||||
|
patched_ex = get_patched_exchange(mocker, conf)
|
||||||
|
prec_price = patched_ex.price_to_precision("XRP/USDT", 1.0000000101)
|
||||||
|
assert prec_price == 1.00000001
|
||||||
|
@@ -4,42 +4,9 @@ from unittest.mock import MagicMock
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.enums import MarginMode, TradingMode
|
from freqtrade.enums import MarginMode, TradingMode
|
||||||
from freqtrade.exceptions import OperationalException
|
|
||||||
from freqtrade.exchange import Gate
|
|
||||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
|
||||||
from tests.conftest import EXMS, get_patched_exchange
|
from tests.conftest import EXMS, get_patched_exchange
|
||||||
|
|
||||||
|
|
||||||
def test_validate_order_types_gate(default_conf, mocker):
|
|
||||||
default_conf['exchange']['name'] = 'gate'
|
|
||||||
mocker.patch(f'{EXMS}._init_ccxt')
|
|
||||||
mocker.patch(f'{EXMS}._load_markets', return_value={})
|
|
||||||
mocker.patch(f'{EXMS}.validate_pairs')
|
|
||||||
mocker.patch(f'{EXMS}.validate_timeframes')
|
|
||||||
mocker.patch(f'{EXMS}.validate_stakecurrency')
|
|
||||||
mocker.patch(f'{EXMS}.validate_pricing')
|
|
||||||
mocker.patch(f'{EXMS}.name', 'Gate')
|
|
||||||
exch = ExchangeResolver.load_exchange('gate', default_conf, True)
|
|
||||||
assert isinstance(exch, Gate)
|
|
||||||
|
|
||||||
default_conf['order_types'] = {
|
|
||||||
'entry': 'market',
|
|
||||||
'exit': 'limit',
|
|
||||||
'stoploss': 'market',
|
|
||||||
'stoploss_on_exchange': False
|
|
||||||
}
|
|
||||||
|
|
||||||
with pytest.raises(OperationalException,
|
|
||||||
match=r'Exchange .* does not support market orders.'):
|
|
||||||
ExchangeResolver.load_exchange('gate', default_conf, True)
|
|
||||||
|
|
||||||
# market-orders supported on futures markets.
|
|
||||||
default_conf['trading_mode'] = 'futures'
|
|
||||||
default_conf['margin_mode'] = 'isolated'
|
|
||||||
ex = ExchangeResolver.load_exchange('gate', default_conf, True)
|
|
||||||
assert ex
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_fetch_stoploss_order_gate(default_conf, mocker):
|
def test_fetch_stoploss_order_gate(default_conf, mocker):
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id='gate')
|
exchange = get_patched_exchange(mocker, default_conf, id='gate')
|
||||||
|
@@ -27,7 +27,7 @@ def test_create_stoploss_order_huobi(default_conf, mocker, limitratio, expected,
|
|||||||
})
|
})
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ def test_create_stoploss_order_dry_run_huobi(default_conf, mocker):
|
|||||||
order_type = 'stop-limit'
|
order_type = 'stop-limit'
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||||
|
|
||||||
|
@@ -29,7 +29,7 @@ def test_buy_kraken_trading_agreement(default_conf, mocker):
|
|||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
|
|
||||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
|
||||||
|
|
||||||
order = exchange.create_order(
|
order = exchange.create_order(
|
||||||
@@ -192,7 +192,7 @@ def test_create_stoploss_order_kraken(default_conf, mocker, ordertype, side, adj
|
|||||||
|
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@ def test_create_stoploss_order_dry_run_kraken(default_conf, mocker, side):
|
|||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||||
|
|
||||||
|
@@ -27,7 +27,7 @@ def test_create_stoploss_order_kucoin(default_conf, mocker, limitratio, expected
|
|||||||
})
|
})
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||||
if order_type == 'limit':
|
if order_type == 'limit':
|
||||||
@@ -88,7 +88,7 @@ def test_stoploss_order_dry_run_kucoin(default_conf, mocker):
|
|||||||
order_type = 'market'
|
order_type = 'market'
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||||
|
|
||||||
|
@@ -119,6 +119,7 @@ def make_unfiltered_dataframe(mocker, freqai_conf):
|
|||||||
freqai = strategy.freqai
|
freqai = strategy.freqai
|
||||||
freqai.live = True
|
freqai.live = True
|
||||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
freqai.dk.live = True
|
||||||
freqai.dk.pair = "ADA/BTC"
|
freqai.dk.pair = "ADA/BTC"
|
||||||
data_load_timerange = TimeRange.parse_timerange("20180110-20180130")
|
data_load_timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||||
freqai.dd.load_all_pair_histories(data_load_timerange, freqai.dk)
|
freqai.dd.load_all_pair_histories(data_load_timerange, freqai.dk)
|
||||||
@@ -152,6 +153,7 @@ def make_data_dictionary(mocker, freqai_conf):
|
|||||||
freqai = strategy.freqai
|
freqai = strategy.freqai
|
||||||
freqai.live = True
|
freqai.live = True
|
||||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
freqai.dk.live = True
|
||||||
freqai.dk.pair = "ADA/BTC"
|
freqai.dk.pair = "ADA/BTC"
|
||||||
data_load_timerange = TimeRange.parse_timerange("20180110-20180130")
|
data_load_timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||||
freqai.dd.load_all_pair_histories(data_load_timerange, freqai.dk)
|
freqai.dd.load_all_pair_histories(data_load_timerange, freqai.dk)
|
||||||
|
@@ -19,6 +19,7 @@ def test_update_historic_data(mocker, freqai_conf):
|
|||||||
freqai = strategy.freqai
|
freqai = strategy.freqai
|
||||||
freqai.live = True
|
freqai.live = True
|
||||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
freqai.dk.live = True
|
||||||
timerange = TimeRange.parse_timerange("20180110-20180114")
|
timerange = TimeRange.parse_timerange("20180110-20180114")
|
||||||
|
|
||||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
@@ -41,6 +42,7 @@ def test_load_all_pairs_histories(mocker, freqai_conf):
|
|||||||
freqai = strategy.freqai
|
freqai = strategy.freqai
|
||||||
freqai.live = True
|
freqai.live = True
|
||||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
freqai.dk.live = True
|
||||||
timerange = TimeRange.parse_timerange("20180110-20180114")
|
timerange = TimeRange.parse_timerange("20180110-20180114")
|
||||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
|
|
||||||
@@ -60,6 +62,7 @@ def test_get_base_and_corr_dataframes(mocker, freqai_conf):
|
|||||||
freqai = strategy.freqai
|
freqai = strategy.freqai
|
||||||
freqai.live = True
|
freqai.live = True
|
||||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
freqai.dk.live = True
|
||||||
timerange = TimeRange.parse_timerange("20180110-20180114")
|
timerange = TimeRange.parse_timerange("20180110-20180114")
|
||||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
sub_timerange = TimeRange.parse_timerange("20180111-20180114")
|
sub_timerange = TimeRange.parse_timerange("20180111-20180114")
|
||||||
@@ -87,6 +90,7 @@ def test_use_strategy_to_populate_indicators(mocker, freqai_conf):
|
|||||||
freqai = strategy.freqai
|
freqai = strategy.freqai
|
||||||
freqai.live = True
|
freqai.live = True
|
||||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
freqai.dk.live = True
|
||||||
timerange = TimeRange.parse_timerange("20180110-20180114")
|
timerange = TimeRange.parse_timerange("20180110-20180114")
|
||||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
sub_timerange = TimeRange.parse_timerange("20180111-20180114")
|
sub_timerange = TimeRange.parse_timerange("20180111-20180114")
|
||||||
@@ -103,8 +107,9 @@ def test_get_timerange_from_live_historic_predictions(mocker, freqai_conf):
|
|||||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||||
strategy.dp = DataProvider(freqai_conf, exchange)
|
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||||
freqai = strategy.freqai
|
freqai = strategy.freqai
|
||||||
freqai.live = True
|
freqai.live = False
|
||||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
freqai.dk.live = False
|
||||||
timerange = TimeRange.parse_timerange("20180126-20180130")
|
timerange = TimeRange.parse_timerange("20180126-20180130")
|
||||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
sub_timerange = TimeRange.parse_timerange("20180128-20180130")
|
sub_timerange = TimeRange.parse_timerange("20180128-20180130")
|
||||||
|
@@ -180,6 +180,7 @@ def test_get_full_model_path(mocker, freqai_conf, model):
|
|||||||
freqai = strategy.freqai
|
freqai = strategy.freqai
|
||||||
freqai.live = True
|
freqai.live = True
|
||||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
freqai.dk.live = True
|
||||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
|
|
||||||
|
@@ -87,6 +87,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
|
|||||||
freqai.live = True
|
freqai.live = True
|
||||||
freqai.can_short = can_short
|
freqai.can_short = can_short
|
||||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
freqai.dk.live = True
|
||||||
freqai.dk.set_paths('ADA/BTC', 10000)
|
freqai.dk.set_paths('ADA/BTC', 10000)
|
||||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
@@ -135,6 +136,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s
|
|||||||
freqai = strategy.freqai
|
freqai = strategy.freqai
|
||||||
freqai.live = True
|
freqai.live = True
|
||||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
freqai.dk.live = True
|
||||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
|
|
||||||
@@ -178,6 +180,7 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model):
|
|||||||
freqai = strategy.freqai
|
freqai = strategy.freqai
|
||||||
freqai.live = True
|
freqai.live = True
|
||||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
freqai.dk.live = True
|
||||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
|
|
||||||
@@ -371,6 +374,9 @@ def test_backtesting_fit_live_predictions(mocker, freqai_conf, caplog):
|
|||||||
sub_timerange = TimeRange.parse_timerange("20180129-20180130")
|
sub_timerange = TimeRange.parse_timerange("20180129-20180130")
|
||||||
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
|
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
|
||||||
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
|
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
|
||||||
|
df = strategy.set_freqai_targets(df.copy(), metadata={"pair": "LTC/BTC"})
|
||||||
|
df = freqai.dk.remove_special_chars_from_feature_names(df)
|
||||||
|
freqai.dk.get_unique_classes_from_labels(df)
|
||||||
freqai.dk.pair = "ADA/BTC"
|
freqai.dk.pair = "ADA/BTC"
|
||||||
freqai.dk.full_df = df.fillna(0)
|
freqai.dk.full_df = df.fillna(0)
|
||||||
freqai.dk.full_df
|
freqai.dk.full_df
|
||||||
@@ -394,6 +400,7 @@ def test_principal_component_analysis(mocker, freqai_conf):
|
|||||||
freqai = strategy.freqai
|
freqai = strategy.freqai
|
||||||
freqai.live = True
|
freqai.live = True
|
||||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
freqai.dk.live = True
|
||||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
|
|
||||||
@@ -425,10 +432,12 @@ def test_plot_feature_importance(mocker, freqai_conf):
|
|||||||
freqai = strategy.freqai
|
freqai = strategy.freqai
|
||||||
freqai.live = True
|
freqai.live = True
|
||||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
freqai.dk.live = True
|
||||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
|
|
||||||
freqai.dd.pair_dict = MagicMock()
|
freqai.dd.pair_dict = {"ADA/BTC": {"model_filename": "fake_name",
|
||||||
|
"trained_timestamp": 1, "data_path": "", "extras": {}}}
|
||||||
|
|
||||||
data_load_timerange = TimeRange.parse_timerange("20180110-20180130")
|
data_load_timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||||
new_timerange = TimeRange.parse_timerange("20180120-20180130")
|
new_timerange = TimeRange.parse_timerange("20180120-20180130")
|
||||||
|
@@ -5,7 +5,7 @@ from unittest.mock import MagicMock
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.data.history import get_timerange
|
from freqtrade.data.history import get_timerange
|
||||||
from freqtrade.enums import ExitType
|
from freqtrade.enums import ExitType, TradingMode
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
from freqtrade.persistence.trade_model import LocalTrade
|
from freqtrade.persistence.trade_model import LocalTrade
|
||||||
from tests.conftest import EXMS, patch_exchange
|
from tests.conftest import EXMS, patch_exchange
|
||||||
@@ -925,12 +925,14 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
|
|||||||
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
||||||
mocker.patch(f"{EXMS}.get_max_leverage", return_value=100)
|
mocker.patch(f"{EXMS}.get_max_leverage", return_value=100)
|
||||||
|
mocker.patch(f"{EXMS}.calculate_funding_fees", return_value=0)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
frame = _build_backtest_dataframe(data.data)
|
frame = _build_backtest_dataframe(data.data)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
# TODO: Should we initialize this properly??
|
# TODO: Should we initialize this properly??
|
||||||
backtesting._can_short = True
|
backtesting.trading_mode = TradingMode.MARGIN
|
||||||
backtesting._set_strategy(backtesting.strategylist[0])
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
|
backtesting._can_short = True
|
||||||
backtesting.required_startup = 0
|
backtesting.required_startup = 0
|
||||||
backtesting.strategy.advise_entry = lambda a, m: frame
|
backtesting.strategy.advise_entry = lambda a, m: frame
|
||||||
backtesting.strategy.advise_exit = lambda a, m: frame
|
backtesting.strategy.advise_exit = lambda a, m: frame
|
||||||
|
@@ -344,7 +344,7 @@ def test_backtest_abort(default_conf, mocker, testdatadir) -> None:
|
|||||||
assert backtesting.progress.progress == 0
|
assert backtesting.progress.progress == 0
|
||||||
|
|
||||||
|
|
||||||
def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
|
def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
||||||
def get_timerange(input1):
|
def get_timerange(input1):
|
||||||
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
||||||
|
|
||||||
@@ -367,6 +367,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
|
|||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting._set_strategy(backtesting.strategylist[0])
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
backtesting.strategy.bot_loop_start = MagicMock()
|
backtesting.strategy.bot_loop_start = MagicMock()
|
||||||
|
backtesting.strategy.bot_start = MagicMock()
|
||||||
backtesting.start()
|
backtesting.start()
|
||||||
# check the logs, that will contain the backtest result
|
# check the logs, that will contain the backtest result
|
||||||
exists = [
|
exists = [
|
||||||
@@ -376,7 +377,8 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
|
|||||||
for line in exists:
|
for line in exists:
|
||||||
assert log_has(line, caplog)
|
assert log_has(line, caplog)
|
||||||
assert backtesting.strategy.dp._pairlists is not None
|
assert backtesting.strategy.dp._pairlists is not None
|
||||||
assert backtesting.strategy.bot_loop_start.call_count == 1
|
assert backtesting.strategy.bot_start.call_count == 1
|
||||||
|
assert backtesting.strategy.bot_loop_start.call_count == 0
|
||||||
assert sbs.call_count == 1
|
assert sbs.call_count == 1
|
||||||
assert sbc.call_count == 1
|
assert sbc.call_count == 1
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ from arrow import Arrow
|
|||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
from freqtrade.data.history import get_timerange
|
from freqtrade.data.history import get_timerange
|
||||||
from freqtrade.enums import ExitType
|
from freqtrade.enums import ExitType, TradingMode
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
from tests.conftest import EXMS, patch_exchange
|
from tests.conftest import EXMS, patch_exchange
|
||||||
|
|
||||||
@@ -108,9 +108,10 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera
|
|||||||
default_conf.update({
|
default_conf.update({
|
||||||
"stake_amount": 100.0,
|
"stake_amount": 100.0,
|
||||||
"dry_run_wallet": 1000.0,
|
"dry_run_wallet": 1000.0,
|
||||||
"strategy": "StrategyTestV3"
|
"strategy": "StrategyTestV3",
|
||||||
})
|
})
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
|
backtesting.trading_mode = TradingMode.FUTURES
|
||||||
backtesting._can_short = True
|
backtesting._can_short = True
|
||||||
backtesting._set_strategy(backtesting.strategylist[0])
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
pair = 'XRP/USDT'
|
pair = 'XRP/USDT'
|
||||||
|
@@ -872,7 +872,8 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
|||||||
hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0)
|
hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0)
|
||||||
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
||||||
assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter)
|
assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter)
|
||||||
assert hyperopt.backtesting.strategy.bot_loop_started is True
|
assert hyperopt.backtesting.strategy.bot_started is True
|
||||||
|
assert hyperopt.backtesting.strategy.bot_loop_started is False
|
||||||
|
|
||||||
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
|
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
|
||||||
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
|
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
|
||||||
@@ -922,7 +923,8 @@ def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir,
|
|||||||
|
|
||||||
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
||||||
assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter)
|
assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter)
|
||||||
assert hyperopt.backtesting.strategy.bot_loop_started is True
|
assert hyperopt.backtesting.strategy.bot_started is True
|
||||||
|
assert hyperopt.backtesting.strategy.bot_loop_started is False
|
||||||
|
|
||||||
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
|
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
|
||||||
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
|
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
|
||||||
@@ -959,7 +961,8 @@ def test_in_strategy_auto_hyperopt_per_epoch(mocker, hyperopt_conf, tmpdir, fee)
|
|||||||
hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0)
|
hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0)
|
||||||
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
||||||
assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter)
|
assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter)
|
||||||
assert hyperopt.backtesting.strategy.bot_loop_started is True
|
assert hyperopt.backtesting.strategy.bot_loop_started is False
|
||||||
|
assert hyperopt.backtesting.strategy.bot_started is True
|
||||||
|
|
||||||
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
|
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
|
||||||
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
|
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
|
||||||
|
@@ -1330,71 +1330,78 @@ def test_to_json(fee):
|
|||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
enter_tag=None,
|
enter_tag=None,
|
||||||
open_order_id='dry_run_buy_12345'
|
open_order_id='dry_run_buy_12345',
|
||||||
|
precision_mode=1,
|
||||||
|
amount_precision=8.0,
|
||||||
|
price_precision=7.0,
|
||||||
)
|
)
|
||||||
result = trade.to_json()
|
result = trade.to_json()
|
||||||
assert isinstance(result, dict)
|
assert isinstance(result, dict)
|
||||||
|
|
||||||
assert result == {'trade_id': None,
|
assert result == {
|
||||||
'pair': 'ADA/USDT',
|
'trade_id': None,
|
||||||
'base_currency': 'ADA',
|
'pair': 'ADA/USDT',
|
||||||
'quote_currency': 'USDT',
|
'base_currency': 'ADA',
|
||||||
'is_open': None,
|
'quote_currency': 'USDT',
|
||||||
'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT),
|
'is_open': None,
|
||||||
'open_timestamp': int(trade.open_date.timestamp() * 1000),
|
'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT),
|
||||||
'open_order_id': 'dry_run_buy_12345',
|
'open_timestamp': int(trade.open_date.timestamp() * 1000),
|
||||||
'close_date': None,
|
'open_order_id': 'dry_run_buy_12345',
|
||||||
'close_timestamp': None,
|
'close_date': None,
|
||||||
'open_rate': 0.123,
|
'close_timestamp': None,
|
||||||
'open_rate_requested': None,
|
'open_rate': 0.123,
|
||||||
'open_trade_value': 15.1668225,
|
'open_rate_requested': None,
|
||||||
'fee_close': 0.0025,
|
'open_trade_value': 15.1668225,
|
||||||
'fee_close_cost': None,
|
'fee_close': 0.0025,
|
||||||
'fee_close_currency': None,
|
'fee_close_cost': None,
|
||||||
'fee_open': 0.0025,
|
'fee_close_currency': None,
|
||||||
'fee_open_cost': None,
|
'fee_open': 0.0025,
|
||||||
'fee_open_currency': None,
|
'fee_open_cost': None,
|
||||||
'close_rate': None,
|
'fee_open_currency': None,
|
||||||
'close_rate_requested': None,
|
'close_rate': None,
|
||||||
'amount': 123.0,
|
'close_rate_requested': None,
|
||||||
'amount_requested': 123.0,
|
'amount': 123.0,
|
||||||
'stake_amount': 0.001,
|
'amount_requested': 123.0,
|
||||||
'max_stake_amount': None,
|
'stake_amount': 0.001,
|
||||||
'trade_duration': None,
|
'max_stake_amount': None,
|
||||||
'trade_duration_s': None,
|
'trade_duration': None,
|
||||||
'realized_profit': 0.0,
|
'trade_duration_s': None,
|
||||||
'realized_profit_ratio': None,
|
'realized_profit': 0.0,
|
||||||
'close_profit': None,
|
'realized_profit_ratio': None,
|
||||||
'close_profit_pct': None,
|
'close_profit': None,
|
||||||
'close_profit_abs': None,
|
'close_profit_pct': None,
|
||||||
'profit_ratio': None,
|
'close_profit_abs': None,
|
||||||
'profit_pct': None,
|
'profit_ratio': None,
|
||||||
'profit_abs': None,
|
'profit_pct': None,
|
||||||
'exit_reason': None,
|
'profit_abs': None,
|
||||||
'exit_order_status': None,
|
'exit_reason': None,
|
||||||
'stop_loss_abs': None,
|
'exit_order_status': None,
|
||||||
'stop_loss_ratio': None,
|
'stop_loss_abs': None,
|
||||||
'stop_loss_pct': None,
|
'stop_loss_ratio': None,
|
||||||
'stoploss_order_id': None,
|
'stop_loss_pct': None,
|
||||||
'stoploss_last_update': None,
|
'stoploss_order_id': None,
|
||||||
'stoploss_last_update_timestamp': None,
|
'stoploss_last_update': None,
|
||||||
'initial_stop_loss_abs': None,
|
'stoploss_last_update_timestamp': None,
|
||||||
'initial_stop_loss_pct': None,
|
'initial_stop_loss_abs': None,
|
||||||
'initial_stop_loss_ratio': None,
|
'initial_stop_loss_pct': None,
|
||||||
'min_rate': None,
|
'initial_stop_loss_ratio': None,
|
||||||
'max_rate': None,
|
'min_rate': None,
|
||||||
'strategy': None,
|
'max_rate': None,
|
||||||
'enter_tag': None,
|
'strategy': None,
|
||||||
'timeframe': None,
|
'enter_tag': None,
|
||||||
'exchange': 'binance',
|
'timeframe': None,
|
||||||
'leverage': None,
|
'exchange': 'binance',
|
||||||
'interest_rate': None,
|
'leverage': None,
|
||||||
'liquidation_price': None,
|
'interest_rate': None,
|
||||||
'is_short': None,
|
'liquidation_price': None,
|
||||||
'trading_mode': None,
|
'is_short': None,
|
||||||
'funding_fees': None,
|
'trading_mode': None,
|
||||||
'orders': [],
|
'funding_fees': None,
|
||||||
}
|
'amount_precision': 8.0,
|
||||||
|
'price_precision': 7.0,
|
||||||
|
'precision_mode': 1,
|
||||||
|
'orders': [],
|
||||||
|
}
|
||||||
|
|
||||||
# Simulate dry_run entries
|
# Simulate dry_run entries
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
@@ -1410,70 +1417,77 @@ def test_to_json(fee):
|
|||||||
close_rate=0.125,
|
close_rate=0.125,
|
||||||
enter_tag='buys_signal_001',
|
enter_tag='buys_signal_001',
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
|
precision_mode=2,
|
||||||
|
amount_precision=7.0,
|
||||||
|
price_precision=8.0,
|
||||||
)
|
)
|
||||||
result = trade.to_json()
|
result = trade.to_json()
|
||||||
assert isinstance(result, dict)
|
assert isinstance(result, dict)
|
||||||
|
|
||||||
assert result == {'trade_id': None,
|
assert result == {
|
||||||
'pair': 'XRP/BTC',
|
'trade_id': None,
|
||||||
'base_currency': 'XRP',
|
'pair': 'XRP/BTC',
|
||||||
'quote_currency': 'BTC',
|
'base_currency': 'XRP',
|
||||||
'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT),
|
'quote_currency': 'BTC',
|
||||||
'open_timestamp': int(trade.open_date.timestamp() * 1000),
|
'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT),
|
||||||
'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT),
|
'open_timestamp': int(trade.open_date.timestamp() * 1000),
|
||||||
'close_timestamp': int(trade.close_date.timestamp() * 1000),
|
'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT),
|
||||||
'open_rate': 0.123,
|
'close_timestamp': int(trade.close_date.timestamp() * 1000),
|
||||||
'close_rate': 0.125,
|
'open_rate': 0.123,
|
||||||
'amount': 100.0,
|
'close_rate': 0.125,
|
||||||
'amount_requested': 101.0,
|
'amount': 100.0,
|
||||||
'stake_amount': 0.001,
|
'amount_requested': 101.0,
|
||||||
'max_stake_amount': None,
|
'stake_amount': 0.001,
|
||||||
'trade_duration': 60,
|
'max_stake_amount': None,
|
||||||
'trade_duration_s': 3600,
|
'trade_duration': 60,
|
||||||
'stop_loss_abs': None,
|
'trade_duration_s': 3600,
|
||||||
'stop_loss_pct': None,
|
'stop_loss_abs': None,
|
||||||
'stop_loss_ratio': None,
|
'stop_loss_pct': None,
|
||||||
'stoploss_order_id': None,
|
'stop_loss_ratio': None,
|
||||||
'stoploss_last_update': None,
|
'stoploss_order_id': None,
|
||||||
'stoploss_last_update_timestamp': None,
|
'stoploss_last_update': None,
|
||||||
'initial_stop_loss_abs': None,
|
'stoploss_last_update_timestamp': None,
|
||||||
'initial_stop_loss_pct': None,
|
'initial_stop_loss_abs': None,
|
||||||
'initial_stop_loss_ratio': None,
|
'initial_stop_loss_pct': None,
|
||||||
'realized_profit': 0.0,
|
'initial_stop_loss_ratio': None,
|
||||||
'realized_profit_ratio': None,
|
'realized_profit': 0.0,
|
||||||
'close_profit': None,
|
'realized_profit_ratio': None,
|
||||||
'close_profit_pct': None,
|
'close_profit': None,
|
||||||
'close_profit_abs': None,
|
'close_profit_pct': None,
|
||||||
'profit_ratio': None,
|
'close_profit_abs': None,
|
||||||
'profit_pct': None,
|
'profit_ratio': None,
|
||||||
'profit_abs': None,
|
'profit_pct': None,
|
||||||
'close_rate_requested': None,
|
'profit_abs': None,
|
||||||
'fee_close': 0.0025,
|
'close_rate_requested': None,
|
||||||
'fee_close_cost': None,
|
'fee_close': 0.0025,
|
||||||
'fee_close_currency': None,
|
'fee_close_cost': None,
|
||||||
'fee_open': 0.0025,
|
'fee_close_currency': None,
|
||||||
'fee_open_cost': None,
|
'fee_open': 0.0025,
|
||||||
'fee_open_currency': None,
|
'fee_open_cost': None,
|
||||||
'is_open': None,
|
'fee_open_currency': None,
|
||||||
'max_rate': None,
|
'is_open': None,
|
||||||
'min_rate': None,
|
'max_rate': None,
|
||||||
'open_order_id': None,
|
'min_rate': None,
|
||||||
'open_rate_requested': None,
|
'open_order_id': None,
|
||||||
'open_trade_value': 12.33075,
|
'open_rate_requested': None,
|
||||||
'exit_reason': None,
|
'open_trade_value': 12.33075,
|
||||||
'exit_order_status': None,
|
'exit_reason': None,
|
||||||
'strategy': None,
|
'exit_order_status': None,
|
||||||
'enter_tag': 'buys_signal_001',
|
'strategy': None,
|
||||||
'timeframe': None,
|
'enter_tag': 'buys_signal_001',
|
||||||
'exchange': 'binance',
|
'timeframe': None,
|
||||||
'leverage': None,
|
'exchange': 'binance',
|
||||||
'interest_rate': None,
|
'leverage': None,
|
||||||
'liquidation_price': None,
|
'interest_rate': None,
|
||||||
'is_short': None,
|
'liquidation_price': None,
|
||||||
'trading_mode': None,
|
'is_short': None,
|
||||||
'funding_fees': None,
|
'trading_mode': None,
|
||||||
'orders': [],
|
'funding_fees': None,
|
||||||
}
|
'amount_precision': 7.0,
|
||||||
|
'price_precision': 8.0,
|
||||||
|
'precision_mode': 2,
|
||||||
|
'orders': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_stoploss_reinitialization(default_conf, fee):
|
def test_stoploss_reinitialization(default_conf, fee):
|
||||||
|
@@ -50,8 +50,8 @@ def test_trade_fromjson():
|
|||||||
"stop_loss_ratio": -0.216,
|
"stop_loss_ratio": -0.216,
|
||||||
"stop_loss_pct": -21.6,
|
"stop_loss_pct": -21.6,
|
||||||
"stoploss_order_id": null,
|
"stoploss_order_id": null,
|
||||||
"stoploss_last_update": null,
|
"stoploss_last_update": "2022-10-18 09:13:42",
|
||||||
"stoploss_last_update_timestamp": null,
|
"stoploss_last_update_timestamp": 1666077222000,
|
||||||
"initial_stop_loss_abs": 0.1981,
|
"initial_stop_loss_abs": 0.1981,
|
||||||
"initial_stop_loss_ratio": -0.216,
|
"initial_stop_loss_ratio": -0.216,
|
||||||
"initial_stop_loss_pct": -21.6,
|
"initial_stop_loss_pct": -21.6,
|
||||||
|
@@ -88,6 +88,9 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'is_short': False,
|
'is_short': False,
|
||||||
'funding_fees': 0.0,
|
'funding_fees': 0.0,
|
||||||
'trading_mode': TradingMode.SPOT,
|
'trading_mode': TradingMode.SPOT,
|
||||||
|
'amount_precision': 8.0,
|
||||||
|
'price_precision': 8.0,
|
||||||
|
'precision_mode': 2,
|
||||||
'orders': [{
|
'orders': [{
|
||||||
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
|
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
|
||||||
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
|
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Unit test file for rpc/api_server.py
|
Unit test file for rpc/api_server.py
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
@@ -299,10 +300,6 @@ def test_api_UvicornServer(mocker):
|
|||||||
s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1'))
|
s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1'))
|
||||||
assert thread_mock.call_count == 0
|
assert thread_mock.call_count == 0
|
||||||
|
|
||||||
s.install_signal_handlers()
|
|
||||||
# Original implementation starts a thread - make sure that's not the case
|
|
||||||
assert thread_mock.call_count == 0
|
|
||||||
|
|
||||||
# Fake started to avoid sleeping forever
|
# Fake started to avoid sleeping forever
|
||||||
s.started = True
|
s.started = True
|
||||||
s.run_in_thread()
|
s.run_in_thread()
|
||||||
@@ -318,10 +315,6 @@ def test_api_UvicornServer_run(mocker):
|
|||||||
s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1'))
|
s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1'))
|
||||||
assert serve_mock.call_count == 0
|
assert serve_mock.call_count == 0
|
||||||
|
|
||||||
s.install_signal_handlers()
|
|
||||||
# Original implementation starts a thread - make sure that's not the case
|
|
||||||
assert serve_mock.call_count == 0
|
|
||||||
|
|
||||||
# Fake started to avoid sleeping forever
|
# Fake started to avoid sleeping forever
|
||||||
s.started = True
|
s.started = True
|
||||||
s.run()
|
s.run()
|
||||||
@@ -331,13 +324,10 @@ def test_api_UvicornServer_run(mocker):
|
|||||||
def test_api_UvicornServer_run_no_uvloop(mocker, import_fails):
|
def test_api_UvicornServer_run_no_uvloop(mocker, import_fails):
|
||||||
serve_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.UvicornServer.serve',
|
serve_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.UvicornServer.serve',
|
||||||
get_mock_coro(None))
|
get_mock_coro(None))
|
||||||
|
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||||
s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1'))
|
s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1'))
|
||||||
assert serve_mock.call_count == 0
|
assert serve_mock.call_count == 0
|
||||||
|
|
||||||
s.install_signal_handlers()
|
|
||||||
# Original implementation starts a thread - make sure that's not the case
|
|
||||||
assert serve_mock.call_count == 0
|
|
||||||
|
|
||||||
# Fake started to avoid sleeping forever
|
# Fake started to avoid sleeping forever
|
||||||
s.started = True
|
s.started = True
|
||||||
s.run()
|
s.run()
|
||||||
@@ -1066,6 +1056,9 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
|||||||
'liquidation_price': None,
|
'liquidation_price': None,
|
||||||
'funding_fees': None,
|
'funding_fees': None,
|
||||||
'trading_mode': ANY,
|
'trading_mode': ANY,
|
||||||
|
'amount_precision': None,
|
||||||
|
'price_precision': None,
|
||||||
|
'precision_mode': None,
|
||||||
'orders': [ANY],
|
'orders': [ANY],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1271,6 +1264,9 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
|||||||
'liquidation_price': None,
|
'liquidation_price': None,
|
||||||
'funding_fees': None,
|
'funding_fees': None,
|
||||||
'trading_mode': 'spot',
|
'trading_mode': 'spot',
|
||||||
|
'amount_precision': None,
|
||||||
|
'price_precision': None,
|
||||||
|
'precision_mode': None,
|
||||||
'orders': [],
|
'orders': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -50,6 +50,7 @@ class HyperoptableStrategy(StrategyTestV3):
|
|||||||
return prot
|
return prot
|
||||||
|
|
||||||
bot_loop_started = False
|
bot_loop_started = False
|
||||||
|
bot_started = False
|
||||||
|
|
||||||
def bot_loop_start(self):
|
def bot_loop_start(self):
|
||||||
self.bot_loop_started = True
|
self.bot_loop_started = True
|
||||||
@@ -58,6 +59,7 @@ class HyperoptableStrategy(StrategyTestV3):
|
|||||||
"""
|
"""
|
||||||
Parameters can also be defined here ...
|
Parameters can also be defined here ...
|
||||||
"""
|
"""
|
||||||
|
self.bot_started = True
|
||||||
self.buy_rsi = IntParameter([0, 50], default=30, space='buy')
|
self.buy_rsi = IntParameter([0, 50], default=30, space='buy')
|
||||||
|
|
||||||
def informative_pairs(self):
|
def informative_pairs(self):
|
||||||
|
@@ -986,7 +986,8 @@ def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mocker.patch('freqtrade.strategy.hyper.json_load', return_value=expected_result)
|
mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params',
|
||||||
|
return_value=expected_result)
|
||||||
PairLocks.timeframe = default_conf['timeframe']
|
PairLocks.timeframe = default_conf['timeframe']
|
||||||
strategy = StrategyResolver.load_strategy(default_conf)
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
assert strategy.stoploss == -0.05
|
assert strategy.stoploss == -0.05
|
||||||
@@ -1005,11 +1006,13 @@ def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mocker.patch('freqtrade.strategy.hyper.json_load', return_value=expected_result)
|
mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params',
|
||||||
|
return_value=expected_result)
|
||||||
with pytest.raises(OperationalException, match="Invalid parameter file provided."):
|
with pytest.raises(OperationalException, match="Invalid parameter file provided."):
|
||||||
StrategyResolver.load_strategy(default_conf)
|
StrategyResolver.load_strategy(default_conf)
|
||||||
|
|
||||||
mocker.patch('freqtrade.strategy.hyper.json_load', MagicMock(side_effect=ValueError()))
|
mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params',
|
||||||
|
MagicMock(side_effect=ValueError()))
|
||||||
|
|
||||||
StrategyResolver.load_strategy(default_conf)
|
StrategyResolver.load_strategy(default_conf)
|
||||||
assert log_has("Invalid parameter file format.", caplog)
|
assert log_has("Invalid parameter file format.", caplog)
|
||||||
|
@@ -356,7 +356,7 @@ def test_create_trade_no_stake_amount(default_conf_usdt, ticker_usdt, fee, mocke
|
|||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
@pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [
|
@pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [
|
||||||
(5.0, True, True, 99),
|
(5.0, True, True, 99),
|
||||||
(0.049, True, False, 99), # Amount will be adjusted to min - which is 0.051
|
(0.042, True, False, 99), # Amount will be adjusted to min - which is 0.051
|
||||||
(0, False, True, 99),
|
(0, False, True, 99),
|
||||||
(UNLIMITED_STAKE_AMOUNT, False, True, 0),
|
(UNLIMITED_STAKE_AMOUNT, False, True, 0),
|
||||||
])
|
])
|
||||||
@@ -1060,9 +1060,19 @@ def test_execute_entry_min_leverage(mocker, default_conf_usdt, fee, limit_order,
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short) -> None:
|
def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short, fee) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
EXMS,
|
||||||
|
fetch_ticker=MagicMock(return_value={
|
||||||
|
'bid': 1.9,
|
||||||
|
'ask': 2.2,
|
||||||
|
'last': 1.9
|
||||||
|
}),
|
||||||
|
create_order=MagicMock(return_value=limit_order[entry_side(is_short)]),
|
||||||
|
get_fee=fee,
|
||||||
|
)
|
||||||
order = limit_order[entry_side(is_short)]
|
order = limit_order[entry_side(is_short)]
|
||||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
|
||||||
mocker.patch(f'{EXMS}.fetch_order', return_value=order)
|
mocker.patch(f'{EXMS}.fetch_order', return_value=order)
|
||||||
@@ -1074,8 +1084,10 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_sho
|
|||||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||||
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
|
||||||
# TODO: should not be magicmock
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
||||||
trade = MagicMock()
|
|
||||||
|
freqtrade.enter_positions()
|
||||||
|
trade = Trade.session.scalars(select(Trade)).first()
|
||||||
trade.is_short = is_short
|
trade.is_short = is_short
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
trade.stoploss_order_id = None
|
trade.stoploss_order_id = None
|
||||||
@@ -1091,7 +1103,8 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_sho
|
|||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_short,
|
def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_short,
|
||||||
limit_order) -> None:
|
limit_order) -> None:
|
||||||
stoploss = MagicMock(return_value={'id': 13434334})
|
stop_order_dict = {'id': "13434334"}
|
||||||
|
stoploss = MagicMock(return_value=stop_order_dict)
|
||||||
enter_order = limit_order[entry_side(is_short)]
|
enter_order = limit_order[entry_side(is_short)]
|
||||||
exit_order = limit_order[exit_side(is_short)]
|
exit_order = limit_order[exit_side(is_short)]
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
@@ -1116,8 +1129,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
|
|||||||
# First case: when stoploss is not yet set but the order is open
|
# First case: when stoploss is not yet set but the order is open
|
||||||
# should get the stoploss order id immediately
|
# should get the stoploss order id immediately
|
||||||
# and should return false as no trade actually happened
|
# and should return false as no trade actually happened
|
||||||
# TODO: should not be magicmock
|
|
||||||
trade = MagicMock()
|
freqtrade.enter_positions()
|
||||||
|
trade = Trade.session.scalars(select(Trade)).first()
|
||||||
trade.is_short = is_short
|
trade.is_short = is_short
|
||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
@@ -1129,44 +1143,62 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
|
|||||||
|
|
||||||
# Second case: when stoploss is set but it is not yet hit
|
# Second case: when stoploss is set but it is not yet hit
|
||||||
# should do nothing and return false
|
# should do nothing and return false
|
||||||
|
stop_order_dict.update({'id': "102"})
|
||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
trade.stoploss_order_id = "100"
|
trade.stoploss_order_id = "102"
|
||||||
|
trade.orders.append(
|
||||||
|
Order(
|
||||||
|
ft_order_side='stoploss',
|
||||||
|
ft_pair=trade.pair,
|
||||||
|
ft_is_open=True,
|
||||||
|
ft_amount=trade.amount,
|
||||||
|
ft_price=trade.stop_loss,
|
||||||
|
order_id='102',
|
||||||
|
status='open',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
hanging_stoploss_order = MagicMock(return_value={'status': 'open'})
|
hanging_stoploss_order = MagicMock(return_value={'status': 'open'})
|
||||||
mocker.patch(f'{EXMS}.fetch_stoploss_order', hanging_stoploss_order)
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', hanging_stoploss_order)
|
||||||
|
|
||||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
assert trade.stoploss_order_id == "100"
|
assert trade.stoploss_order_id == "102"
|
||||||
|
|
||||||
# Third case: when stoploss was set but it was canceled for some reason
|
# Third case: when stoploss was set but it was canceled for some reason
|
||||||
# should set a stoploss immediately and return False
|
# should set a stoploss immediately and return False
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
trade.stoploss_order_id = "100"
|
trade.stoploss_order_id = "102"
|
||||||
|
|
||||||
canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'})
|
canceled_stoploss_order = MagicMock(return_value={'id': '103_1', 'status': 'canceled'})
|
||||||
mocker.patch(f'{EXMS}.fetch_stoploss_order', canceled_stoploss_order)
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', canceled_stoploss_order)
|
||||||
stoploss.reset_mock()
|
stoploss.reset_mock()
|
||||||
|
amount_before = trade.amount
|
||||||
|
|
||||||
|
stop_order_dict.update({'id': "103_1"})
|
||||||
|
|
||||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
assert stoploss.call_count == 1
|
assert stoploss.call_count == 1
|
||||||
assert trade.stoploss_order_id == "13434334"
|
assert trade.stoploss_order_id == "103_1"
|
||||||
|
assert trade.amount == amount_before
|
||||||
|
|
||||||
# Fourth case: when stoploss is set and it is hit
|
# Fourth case: when stoploss is set and it is hit
|
||||||
# should unset stoploss_order_id and return true
|
# should unset stoploss_order_id and return true
|
||||||
# as a trade actually happened
|
# as a trade actually happened
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
freqtrade.enter_positions()
|
freqtrade.enter_positions()
|
||||||
|
stop_order_dict.update({'id': "104"})
|
||||||
|
|
||||||
trade = Trade.session.scalars(select(Trade)).first()
|
trade = Trade.session.scalars(select(Trade)).first()
|
||||||
trade.is_short = is_short
|
trade.is_short = is_short
|
||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
trade.stoploss_order_id = "100"
|
trade.stoploss_order_id = "104"
|
||||||
trade.orders.append(Order(
|
trade.orders.append(Order(
|
||||||
ft_order_side='stoploss',
|
ft_order_side='stoploss',
|
||||||
order_id='100',
|
order_id='104',
|
||||||
ft_pair=trade.pair,
|
ft_pair=trade.pair,
|
||||||
ft_is_open=True,
|
ft_is_open=True,
|
||||||
ft_amount=trade.amount,
|
ft_amount=trade.amount,
|
||||||
@@ -1175,7 +1207,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
|
|||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
stoploss_order_hit = MagicMock(return_value={
|
stoploss_order_hit = MagicMock(return_value={
|
||||||
'id': "100",
|
'id': "104",
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'type': 'stop_loss_limit',
|
'type': 'stop_loss_limit',
|
||||||
'price': 3,
|
'price': 3,
|
||||||
@@ -1197,7 +1229,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
|
|||||||
|
|
||||||
# Fifth case: fetch_order returns InvalidOrder
|
# Fifth case: fetch_order returns InvalidOrder
|
||||||
# It should try to add stoploss order
|
# It should try to add stoploss order
|
||||||
trade.stoploss_order_id = 100
|
stop_order_dict.update({'id': "105"})
|
||||||
|
trade.stoploss_order_id = "105"
|
||||||
stoploss.reset_mock()
|
stoploss.reset_mock()
|
||||||
mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=InvalidOrderException())
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=InvalidOrderException())
|
||||||
mocker.patch(f'{EXMS}.create_stoploss', stoploss)
|
mocker.patch(f'{EXMS}.create_stoploss', stoploss)
|
||||||
@@ -1217,21 +1250,36 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
|
|||||||
# Seventh case: emergency exit triggered
|
# Seventh case: emergency exit triggered
|
||||||
# Trailing stop should not act anymore
|
# Trailing stop should not act anymore
|
||||||
stoploss_order_cancelled = MagicMock(side_effect=[{
|
stoploss_order_cancelled = MagicMock(side_effect=[{
|
||||||
'id': "100",
|
'id': "107",
|
||||||
'status': 'canceled',
|
'status': 'canceled',
|
||||||
'type': 'stop_loss_limit',
|
'type': 'stop_loss_limit',
|
||||||
'price': 3,
|
'price': 3,
|
||||||
'average': 2,
|
'average': 2,
|
||||||
'amount': enter_order['amount'],
|
'amount': enter_order['amount'],
|
||||||
|
'filled': 0,
|
||||||
|
'remaining': enter_order['amount'],
|
||||||
'info': {'stopPrice': 22},
|
'info': {'stopPrice': 22},
|
||||||
}])
|
}])
|
||||||
trade.stoploss_order_id = 100
|
trade.stoploss_order_id = "107"
|
||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
trade.stoploss_last_update = arrow.utcnow().shift(hours=-1).datetime
|
trade.stoploss_last_update = arrow.utcnow().shift(hours=-1).datetime
|
||||||
trade.stop_loss = 24
|
trade.stop_loss = 24
|
||||||
|
trade.exit_reason = None
|
||||||
|
trade.orders.append(
|
||||||
|
Order(
|
||||||
|
ft_order_side='stoploss',
|
||||||
|
ft_pair=trade.pair,
|
||||||
|
ft_is_open=True,
|
||||||
|
ft_amount=trade.amount,
|
||||||
|
ft_price=trade.stop_loss,
|
||||||
|
order_id='107',
|
||||||
|
status='open',
|
||||||
|
)
|
||||||
|
)
|
||||||
freqtrade.config['trailing_stop'] = True
|
freqtrade.config['trailing_stop'] = True
|
||||||
stoploss = MagicMock(side_effect=InvalidOrderException())
|
stoploss = MagicMock(side_effect=InvalidOrderException())
|
||||||
|
|
||||||
|
Trade.commit()
|
||||||
mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result',
|
mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result',
|
||||||
side_effect=InvalidOrderException())
|
side_effect=InvalidOrderException())
|
||||||
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_cancelled)
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_cancelled)
|
||||||
@@ -1242,6 +1290,137 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
|
|||||||
assert trade.exit_reason == str(ExitType.EMERGENCY_EXIT)
|
assert trade.exit_reason == str(ExitType.EMERGENCY_EXIT)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
|
def test_handle_stoploss_on_exchange_partial(
|
||||||
|
mocker, default_conf_usdt, fee, is_short, limit_order) -> None:
|
||||||
|
stop_order_dict = {'id': "101", "status": "open"}
|
||||||
|
stoploss = MagicMock(return_value=stop_order_dict)
|
||||||
|
enter_order = limit_order[entry_side(is_short)]
|
||||||
|
exit_order = limit_order[exit_side(is_short)]
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
EXMS,
|
||||||
|
fetch_ticker=MagicMock(return_value={
|
||||||
|
'bid': 1.9,
|
||||||
|
'ask': 2.2,
|
||||||
|
'last': 1.9
|
||||||
|
}),
|
||||||
|
create_order=MagicMock(side_effect=[
|
||||||
|
enter_order,
|
||||||
|
exit_order,
|
||||||
|
]),
|
||||||
|
get_fee=fee,
|
||||||
|
create_stoploss=stoploss
|
||||||
|
)
|
||||||
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||||
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
||||||
|
|
||||||
|
freqtrade.enter_positions()
|
||||||
|
trade = Trade.session.scalars(select(Trade)).first()
|
||||||
|
trade.is_short = is_short
|
||||||
|
trade.is_open = True
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.stoploss_order_id = None
|
||||||
|
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
assert stoploss.call_count == 1
|
||||||
|
assert trade.stoploss_order_id == "101"
|
||||||
|
assert trade.amount == 30
|
||||||
|
stop_order_dict.update({'id': "102"})
|
||||||
|
# Stoploss on exchange is cancelled on exchange, but filled partially.
|
||||||
|
# Must update trade amount to guarantee successful exit.
|
||||||
|
stoploss_order_hit = MagicMock(return_value={
|
||||||
|
'id': "101",
|
||||||
|
'status': 'canceled',
|
||||||
|
'type': 'stop_loss_limit',
|
||||||
|
'price': 3,
|
||||||
|
'average': 2,
|
||||||
|
'filled': trade.amount / 2,
|
||||||
|
'remaining': trade.amount / 2,
|
||||||
|
'amount': enter_order['amount'],
|
||||||
|
})
|
||||||
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
# Stoploss filled partially ...
|
||||||
|
assert trade.amount == 15
|
||||||
|
|
||||||
|
assert trade.stoploss_order_id == "102"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
|
def test_handle_stoploss_on_exchange_partial_cancel_here(
|
||||||
|
mocker, default_conf_usdt, fee, is_short, limit_order, caplog) -> None:
|
||||||
|
stop_order_dict = {'id': "101", "status": "open"}
|
||||||
|
default_conf_usdt['trailing_stop'] = True
|
||||||
|
stoploss = MagicMock(return_value=stop_order_dict)
|
||||||
|
enter_order = limit_order[entry_side(is_short)]
|
||||||
|
exit_order = limit_order[exit_side(is_short)]
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
EXMS,
|
||||||
|
fetch_ticker=MagicMock(return_value={
|
||||||
|
'bid': 1.9,
|
||||||
|
'ask': 2.2,
|
||||||
|
'last': 1.9
|
||||||
|
}),
|
||||||
|
create_order=MagicMock(side_effect=[
|
||||||
|
enter_order,
|
||||||
|
exit_order,
|
||||||
|
]),
|
||||||
|
get_fee=fee,
|
||||||
|
create_stoploss=stoploss
|
||||||
|
)
|
||||||
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||||
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
||||||
|
|
||||||
|
freqtrade.enter_positions()
|
||||||
|
trade = Trade.session.scalars(select(Trade)).first()
|
||||||
|
trade.is_short = is_short
|
||||||
|
trade.is_open = True
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.stoploss_order_id = None
|
||||||
|
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
assert stoploss.call_count == 1
|
||||||
|
assert trade.stoploss_order_id == "101"
|
||||||
|
assert trade.amount == 30
|
||||||
|
stop_order_dict.update({'id': "102"})
|
||||||
|
# Stoploss on exchange is open.
|
||||||
|
# Freqtrade cancels the stop - but cancel returns a partial filled order.
|
||||||
|
stoploss_order_hit = MagicMock(return_value={
|
||||||
|
'id': "101",
|
||||||
|
'status': 'open',
|
||||||
|
'type': 'stop_loss_limit',
|
||||||
|
'price': 3,
|
||||||
|
'average': 2,
|
||||||
|
'filled': 0,
|
||||||
|
'remaining': trade.amount,
|
||||||
|
'amount': enter_order['amount'],
|
||||||
|
})
|
||||||
|
stoploss_order_cancel = MagicMock(return_value={
|
||||||
|
'id': "101",
|
||||||
|
'status': 'canceled',
|
||||||
|
'type': 'stop_loss_limit',
|
||||||
|
'price': 3,
|
||||||
|
'average': 2,
|
||||||
|
'filled': trade.amount / 2,
|
||||||
|
'remaining': trade.amount / 2,
|
||||||
|
'amount': enter_order['amount'],
|
||||||
|
})
|
||||||
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
|
||||||
|
mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', stoploss_order_cancel)
|
||||||
|
trade.stoploss_last_update = arrow.utcnow().shift(minutes=-10).datetime
|
||||||
|
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
# Canceled Stoploss filled partially ...
|
||||||
|
assert log_has_re('Cancelling current stoploss on exchange.*', caplog)
|
||||||
|
|
||||||
|
assert trade.stoploss_order_id == "102"
|
||||||
|
assert trade.amount == 15
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, is_short,
|
def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, is_short,
|
||||||
limit_order) -> None:
|
limit_order) -> None:
|
||||||
@@ -1273,10 +1452,21 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
|
|||||||
|
|
||||||
freqtrade.enter_positions()
|
freqtrade.enter_positions()
|
||||||
trade = Trade.session.scalars(select(Trade)).first()
|
trade = Trade.session.scalars(select(Trade)).first()
|
||||||
trade.is_short = is_short
|
assert trade.is_short == is_short
|
||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
trade.stoploss_order_id = 100
|
trade.stoploss_order_id = "100"
|
||||||
|
trade.orders.append(
|
||||||
|
Order(
|
||||||
|
ft_order_side='stoploss',
|
||||||
|
ft_pair=trade.pair,
|
||||||
|
ft_is_open=True,
|
||||||
|
ft_amount=trade.amount,
|
||||||
|
ft_price=trade.stop_loss,
|
||||||
|
order_id='100',
|
||||||
|
status='open',
|
||||||
|
)
|
||||||
|
)
|
||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
@@ -1395,7 +1585,7 @@ def test_handle_stoploss_on_exchange_trailing(
|
|||||||
# When trailing stoploss is set
|
# When trailing stoploss is set
|
||||||
enter_order = limit_order[entry_side(is_short)]
|
enter_order = limit_order[entry_side(is_short)]
|
||||||
exit_order = limit_order[exit_side(is_short)]
|
exit_order = limit_order[exit_side(is_short)]
|
||||||
stoploss = MagicMock(return_value={'id': 13434334})
|
stoploss = MagicMock(return_value={'id': 13434334, 'status': 'open'})
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
EXMS,
|
EXMS,
|
||||||
@@ -1440,11 +1630,21 @@ def test_handle_stoploss_on_exchange_trailing(
|
|||||||
trade.is_short = is_short
|
trade.is_short = is_short
|
||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
trade.stoploss_order_id = 100
|
trade.stoploss_order_id = '100'
|
||||||
trade.stoploss_last_update = arrow.utcnow().shift(minutes=-20).datetime
|
trade.stoploss_last_update = arrow.utcnow().shift(minutes=-20).datetime
|
||||||
|
trade.orders.append(
|
||||||
|
Order(
|
||||||
|
ft_order_side='stoploss',
|
||||||
|
ft_pair=trade.pair,
|
||||||
|
ft_is_open=True,
|
||||||
|
ft_amount=trade.amount,
|
||||||
|
ft_price=trade.stop_loss,
|
||||||
|
order_id='100',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
stoploss_order_hanging = MagicMock(return_value={
|
stoploss_order_hanging = MagicMock(return_value={
|
||||||
'id': 100,
|
'id': '100',
|
||||||
'status': 'open',
|
'status': 'open',
|
||||||
'type': 'stop_loss_limit',
|
'type': 'stop_loss_limit',
|
||||||
'price': hang_price,
|
'price': hang_price,
|
||||||
@@ -1471,7 +1671,7 @@ def test_handle_stoploss_on_exchange_trailing(
|
|||||||
)
|
)
|
||||||
|
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
stoploss_order_mock = MagicMock(return_value={'id': 'so1'})
|
stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'})
|
||||||
mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock)
|
mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock)
|
||||||
mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock)
|
mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock)
|
||||||
|
|
||||||
@@ -1483,13 +1683,14 @@ def test_handle_stoploss_on_exchange_trailing(
|
|||||||
|
|
||||||
assert freqtrade.handle_trade(trade) is False
|
assert freqtrade.handle_trade(trade) is False
|
||||||
assert trade.stop_loss == stop_price[1]
|
assert trade.stop_loss == stop_price[1]
|
||||||
|
trade.stoploss_order_id = '100'
|
||||||
|
|
||||||
# setting stoploss_on_exchange_interval to 0 seconds
|
# setting stoploss_on_exchange_interval to 0 seconds
|
||||||
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
|
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
|
||||||
|
|
||||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
|
||||||
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
cancel_order_mock.assert_called_once_with('100', 'ETH/USDT')
|
||||||
stoploss_order_mock.assert_called_once_with(
|
stoploss_order_mock.assert_called_once_with(
|
||||||
amount=pytest.approx(amt),
|
amount=pytest.approx(amt),
|
||||||
pair='ETH/USDT',
|
pair='ETH/USDT',
|
||||||
@@ -1519,7 +1720,7 @@ def test_handle_stoploss_on_exchange_trailing_error(
|
|||||||
enter_order = limit_order[entry_side(is_short)]
|
enter_order = limit_order[entry_side(is_short)]
|
||||||
exit_order = limit_order[exit_side(is_short)]
|
exit_order = limit_order[exit_side(is_short)]
|
||||||
# When trailing stoploss is set
|
# When trailing stoploss is set
|
||||||
stoploss = MagicMock(return_value={'id': 13434334})
|
stoploss = MagicMock(return_value={'id': '13434334', 'status': 'open'})
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@@ -1601,7 +1802,7 @@ def test_stoploss_on_exchange_price_rounding(
|
|||||||
EXMS,
|
EXMS,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
price_mock = MagicMock(side_effect=lambda p, s: int(s))
|
price_mock = MagicMock(side_effect=lambda p, s, **kwargs: int(s))
|
||||||
stoploss_mock = MagicMock(return_value={'id': '13434334'})
|
stoploss_mock = MagicMock(return_value={'id': '13434334'})
|
||||||
adjust_mock = MagicMock(return_value=False)
|
adjust_mock = MagicMock(return_value=False)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@@ -1628,7 +1829,7 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
|||||||
enter_order = limit_order[entry_side(is_short)]
|
enter_order = limit_order[entry_side(is_short)]
|
||||||
exit_order = limit_order[exit_side(is_short)]
|
exit_order = limit_order[exit_side(is_short)]
|
||||||
# When trailing stoploss is set
|
# When trailing stoploss is set
|
||||||
stoploss = MagicMock(return_value={'id': 13434334})
|
stoploss = MagicMock(return_value={'id': 13434334, 'status': 'open'})
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
EXMS,
|
EXMS,
|
||||||
@@ -1673,11 +1874,21 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
|||||||
trade.is_short = is_short
|
trade.is_short = is_short
|
||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
trade.stoploss_order_id = 100
|
trade.stoploss_order_id = '100'
|
||||||
trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime
|
trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime
|
||||||
|
trade.orders.append(
|
||||||
|
Order(
|
||||||
|
ft_order_side='stoploss',
|
||||||
|
ft_pair=trade.pair,
|
||||||
|
ft_is_open=True,
|
||||||
|
ft_amount=trade.amount,
|
||||||
|
ft_price=trade.stop_loss,
|
||||||
|
order_id='100',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
stoploss_order_hanging = MagicMock(return_value={
|
stoploss_order_hanging = MagicMock(return_value={
|
||||||
'id': 100,
|
'id': '100',
|
||||||
'status': 'open',
|
'status': 'open',
|
||||||
'type': 'stop_loss_limit',
|
'type': 'stop_loss_limit',
|
||||||
'price': 3,
|
'price': 3,
|
||||||
@@ -1703,9 +1914,10 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
|||||||
)
|
)
|
||||||
|
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
stoploss_order_mock = MagicMock(return_value={'id': 'so1'})
|
stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'})
|
||||||
mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock)
|
mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock)
|
||||||
mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock)
|
mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock)
|
||||||
|
trade.stoploss_order_id = '100'
|
||||||
|
|
||||||
# stoploss should not be updated as the interval is 60 seconds
|
# stoploss should not be updated as the interval is 60 seconds
|
||||||
assert freqtrade.handle_trade(trade) is False
|
assert freqtrade.handle_trade(trade) is False
|
||||||
@@ -1722,7 +1934,7 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
|||||||
|
|
||||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
|
||||||
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
cancel_order_mock.assert_called_once_with('100', 'ETH/USDT')
|
||||||
# Long uses modified ask - offset, short modified bid + offset
|
# Long uses modified ask - offset, short modified bid + offset
|
||||||
stoploss_order_mock.assert_called_once_with(
|
stoploss_order_mock.assert_called_once_with(
|
||||||
amount=pytest.approx(trade.amount),
|
amount=pytest.approx(trade.amount),
|
||||||
@@ -1751,7 +1963,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
|
|||||||
exit_order = limit_order['sell']
|
exit_order = limit_order['sell']
|
||||||
|
|
||||||
# When trailing stoploss is set
|
# When trailing stoploss is set
|
||||||
stoploss = MagicMock(return_value={'id': 13434334})
|
stoploss = MagicMock(return_value={'id': '13434334', 'status': 'open'})
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
patch_edge(mocker)
|
patch_edge(mocker)
|
||||||
@@ -1800,11 +2012,21 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
|
|||||||
trade = Trade.session.scalars(select(Trade)).first()
|
trade = Trade.session.scalars(select(Trade)).first()
|
||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
trade.stoploss_order_id = 100
|
trade.stoploss_order_id = '100'
|
||||||
trade.stoploss_last_update = arrow.utcnow()
|
trade.stoploss_last_update = arrow.utcnow().datetime
|
||||||
|
trade.orders.append(
|
||||||
|
Order(
|
||||||
|
ft_order_side='stoploss',
|
||||||
|
ft_pair=trade.pair,
|
||||||
|
ft_is_open=True,
|
||||||
|
ft_amount=trade.amount,
|
||||||
|
ft_price=trade.stop_loss,
|
||||||
|
order_id='100',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
stoploss_order_hanging = MagicMock(return_value={
|
stoploss_order_hanging = MagicMock(return_value={
|
||||||
'id': 100,
|
'id': '100',
|
||||||
'status': 'open',
|
'status': 'open',
|
||||||
'type': 'stop_loss_limit',
|
'type': 'stop_loss_limit',
|
||||||
'price': 3,
|
'price': 3,
|
||||||
@@ -1851,7 +2073,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
|
|||||||
|
|
||||||
# stoploss should be set to 1% as trailing is on
|
# stoploss should be set to 1% as trailing is on
|
||||||
assert trade.stop_loss == 4.4 * 0.99
|
assert trade.stop_loss == 4.4 * 0.99
|
||||||
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
|
cancel_order_mock.assert_called_once_with('100', 'NEO/BTC')
|
||||||
stoploss_order_mock.assert_called_once_with(
|
stoploss_order_mock.assert_called_once_with(
|
||||||
amount=pytest.approx(11.41438356),
|
amount=pytest.approx(11.41438356),
|
||||||
pair='NEO/BTC',
|
pair='NEO/BTC',
|
||||||
@@ -2733,6 +2955,9 @@ def test_manage_open_orders_exit_usercustom(
|
|||||||
assert rpc_mock.call_count == 2
|
assert rpc_mock.call_count == 2
|
||||||
assert freqtrade.strategy.check_exit_timeout.call_count == 1
|
assert freqtrade.strategy.check_exit_timeout.call_count == 1
|
||||||
assert freqtrade.strategy.check_entry_timeout.call_count == 0
|
assert freqtrade.strategy.check_entry_timeout.call_count == 0
|
||||||
|
trade = Trade.session.scalars(select(Trade)).first()
|
||||||
|
# cancelling didn't succeed - order-id remains open.
|
||||||
|
assert trade.open_order_id is not None
|
||||||
|
|
||||||
# 2nd canceled trade - Fail execute exit
|
# 2nd canceled trade - Fail execute exit
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
@@ -3243,6 +3468,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
|
|||||||
|
|
||||||
# TODO: should not be magicmock
|
# TODO: should not be magicmock
|
||||||
trade = MagicMock()
|
trade = MagicMock()
|
||||||
|
trade.open_order_id = '125'
|
||||||
reason = CANCEL_REASON['TIMEOUT']
|
reason = CANCEL_REASON['TIMEOUT']
|
||||||
order = {'remaining': 1,
|
order = {'remaining': 1,
|
||||||
'id': '125',
|
'id': '125',
|
||||||
@@ -3250,6 +3476,10 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
|
|||||||
'status': "open"}
|
'status': "open"}
|
||||||
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
||||||
|
|
||||||
|
# mocker.patch(f'{EXMS}.cancel_order_with_result', return_value=order)
|
||||||
|
# assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
||||||
|
# assert trade.open_order_id == '125'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("is_short, open_rate, amt", [
|
@pytest.mark.parametrize("is_short, open_rate, amt", [
|
||||||
(False, 2.0, 30.0),
|
(False, 2.0, 30.0),
|
||||||
@@ -3326,6 +3556,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
|
|||||||
'profit_ratio': 0.00493809 if is_short else 0.09451372,
|
'profit_ratio': 0.00493809 if is_short else 0.09451372,
|
||||||
'stake_currency': 'USDT',
|
'stake_currency': 'USDT',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
|
'base_currency': 'ETH',
|
||||||
'sell_reason': ExitType.ROI.value,
|
'sell_reason': ExitType.ROI.value,
|
||||||
'exit_reason': ExitType.ROI.value,
|
'exit_reason': ExitType.ROI.value,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
@@ -3389,6 +3620,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
|
|||||||
'profit_amount': -5.65990099 if is_short else -0.00075,
|
'profit_amount': -5.65990099 if is_short else -0.00075,
|
||||||
'profit_ratio': -0.0945681 if is_short else -1.247e-05,
|
'profit_ratio': -0.0945681 if is_short else -1.247e-05,
|
||||||
'stake_currency': 'USDT',
|
'stake_currency': 'USDT',
|
||||||
|
'base_currency': 'ETH',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
'sell_reason': ExitType.STOP_LOSS.value,
|
'sell_reason': ExitType.STOP_LOSS.value,
|
||||||
'exit_reason': ExitType.STOP_LOSS.value,
|
'exit_reason': ExitType.STOP_LOSS.value,
|
||||||
@@ -3474,6 +3706,7 @@ def test_execute_trade_exit_custom_exit_price(
|
|||||||
'profit_amount': pytest.approx(profit_amount),
|
'profit_amount': pytest.approx(profit_amount),
|
||||||
'profit_ratio': profit_ratio,
|
'profit_ratio': profit_ratio,
|
||||||
'stake_currency': 'USDT',
|
'stake_currency': 'USDT',
|
||||||
|
'base_currency': 'ETH',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
'sell_reason': 'foo',
|
'sell_reason': 'foo',
|
||||||
'exit_reason': 'foo',
|
'exit_reason': 'foo',
|
||||||
@@ -3547,6 +3780,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
|
|||||||
'profit_ratio': -0.00501253 if is_short else -0.01493766,
|
'profit_ratio': -0.00501253 if is_short else -0.01493766,
|
||||||
'stake_currency': 'USDT',
|
'stake_currency': 'USDT',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
|
'base_currency': 'ETH',
|
||||||
'sell_reason': ExitType.STOP_LOSS.value,
|
'sell_reason': ExitType.STOP_LOSS.value,
|
||||||
'exit_reason': ExitType.STOP_LOSS.value,
|
'exit_reason': ExitType.STOP_LOSS.value,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
@@ -3588,7 +3822,7 @@ def test_execute_trade_exit_sloe_cancel_exception(
|
|||||||
freqtrade.execute_trade_exit(trade=trade, limit=1234,
|
freqtrade.execute_trade_exit(trade=trade, limit=1234,
|
||||||
exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS))
|
exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS))
|
||||||
assert create_order_mock.call_count == 2
|
assert create_order_mock.call_count == 2
|
||||||
assert log_has('Could not cancel stoploss order abcd', caplog)
|
assert log_has('Could not cancel stoploss order abcd for pair ETH/USDT', caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
@@ -3600,10 +3834,12 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
|
|||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
stoploss = MagicMock(return_value={
|
stoploss = MagicMock(return_value={
|
||||||
'id': 123,
|
'id': 123,
|
||||||
|
'status': 'open',
|
||||||
'info': {
|
'info': {
|
||||||
'foo': 'bar'
|
'foo': 'bar'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_order_fee')
|
||||||
|
|
||||||
cancel_order = MagicMock(return_value=True)
|
cancel_order = MagicMock(return_value=True)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@@ -3701,12 +3937,12 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
|
|||||||
"lastTradeTimestamp": None,
|
"lastTradeTimestamp": None,
|
||||||
"symbol": "BTC/USDT",
|
"symbol": "BTC/USDT",
|
||||||
"type": "stop_loss_limit",
|
"type": "stop_loss_limit",
|
||||||
"side": "sell",
|
"side": "buy" if is_short else "sell",
|
||||||
"price": 1.08801,
|
"price": 1.08801,
|
||||||
"amount": 90.99181074,
|
"amount": trade.amount,
|
||||||
"cost": 99.0000000032274,
|
"cost": 1.08801 * trade.amount,
|
||||||
"average": 1.08801,
|
"average": 1.08801,
|
||||||
"filled": 90.99181074,
|
"filled": trade.amount,
|
||||||
"remaining": 0.0,
|
"remaining": 0.0,
|
||||||
"status": "closed",
|
"status": "closed",
|
||||||
"fee": None,
|
"fee": None,
|
||||||
@@ -3811,6 +4047,7 @@ def test_execute_trade_exit_market_order(
|
|||||||
'profit_amount': pytest.approx(profit_amount),
|
'profit_amount': pytest.approx(profit_amount),
|
||||||
'profit_ratio': profit_ratio,
|
'profit_ratio': profit_ratio,
|
||||||
'stake_currency': 'USDT',
|
'stake_currency': 'USDT',
|
||||||
|
'base_currency': 'ETH',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
'sell_reason': ExitType.ROI.value,
|
'sell_reason': ExitType.ROI.value,
|
||||||
'exit_reason': ExitType.ROI.value,
|
'exit_reason': ExitType.ROI.value,
|
||||||
|
@@ -35,7 +35,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
|||||||
"type": "stop_loss_limit",
|
"type": "stop_loss_limit",
|
||||||
"side": "sell",
|
"side": "sell",
|
||||||
"price": 1.08801,
|
"price": 1.08801,
|
||||||
"amount": 90.99181074,
|
"amount": 91.07468123,
|
||||||
"cost": 0.0,
|
"cost": 0.0,
|
||||||
"average": 0.0,
|
"average": 0.0,
|
||||||
"filled": 0.0,
|
"filled": 0.0,
|
||||||
@@ -49,8 +49,9 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
|||||||
stoploss_order_closed['filled'] = stoploss_order_closed['amount']
|
stoploss_order_closed['filled'] = stoploss_order_closed['amount']
|
||||||
|
|
||||||
# Sell first trade based on stoploss, keep 2nd and 3rd trade open
|
# Sell first trade based on stoploss, keep 2nd and 3rd trade open
|
||||||
|
stop_orders = [stoploss_order_closed, stoploss_order_open, stoploss_order_open]
|
||||||
stoploss_order_mock = MagicMock(
|
stoploss_order_mock = MagicMock(
|
||||||
side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open])
|
side_effect=stop_orders)
|
||||||
# Sell 3rd trade (not called for the first trade)
|
# Sell 3rd trade (not called for the first trade)
|
||||||
should_sell_mock = MagicMock(side_effect=[
|
should_sell_mock = MagicMock(side_effect=[
|
||||||
[],
|
[],
|
||||||
@@ -93,13 +94,14 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
|||||||
wallets_mock.reset_mock()
|
wallets_mock.reset_mock()
|
||||||
|
|
||||||
trades = Trade.session.scalars(select(Trade)).all()
|
trades = Trade.session.scalars(select(Trade)).all()
|
||||||
# Make sure stoploss-order is open and trade is bought (since we mock update_trade_state)
|
# Make sure stoploss-order is open and trade is bought
|
||||||
for trade in trades:
|
for idx, trade in enumerate(trades):
|
||||||
stoploss_order_closed['id'] = '3'
|
stop_order = stop_orders[idx]
|
||||||
oobj = Order.parse_from_ccxt_object(stoploss_order_closed, trade.pair, 'stoploss')
|
stop_order['id'] = f"stop{idx}"
|
||||||
|
oobj = Order.parse_from_ccxt_object(stop_order, trade.pair, 'stoploss')
|
||||||
|
|
||||||
trade.orders.append(oobj)
|
trade.orders.append(oobj)
|
||||||
trade.stoploss_order_id = '3'
|
trade.stoploss_order_id = f"stop{idx}"
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
|
|
||||||
n = freqtrade.exit_positions(trades)
|
n = freqtrade.exit_positions(trades)
|
||||||
|
BIN
tests/testdata/XRP_ETH-trades.feather
vendored
Normal file
BIN
tests/testdata/XRP_ETH-trades.feather
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user