Merge remote-tracking branch 'origin/develop' into gc_improvements
This commit is contained in:
commit
aceee67e2b
63
.github/workflows/ci.yml
vendored
63
.github/workflows/ci.yml
vendored
@ -66,12 +66,6 @@ jobs:
|
|||||||
- name: Tests
|
- name: Tests
|
||||||
run: |
|
run: |
|
||||||
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
||||||
if: matrix.python-version != '3.9' || matrix.os != 'ubuntu-22.04'
|
|
||||||
|
|
||||||
- name: Tests incl. ccxt compatibility tests
|
|
||||||
run: |
|
|
||||||
pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun
|
|
||||||
if: matrix.python-version == '3.9' && matrix.os == 'ubuntu-22.04'
|
|
||||||
|
|
||||||
- name: Coveralls
|
- name: Coveralls
|
||||||
if: (runner.os == 'Linux' && matrix.python-version == '3.10' && matrix.os == 'ubuntu-22.04')
|
if: (runner.os == 'Linux' && matrix.python-version == '3.10' && matrix.os == 'ubuntu-22.04')
|
||||||
@ -310,9 +304,64 @@ jobs:
|
|||||||
details: Freqtrade doc test failed!
|
details: Freqtrade doc test failed!
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
|
|
||||||
|
build_linux_online:
|
||||||
|
# Run pytest with "live" checks
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
# permissions:
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.9"
|
||||||
|
|
||||||
|
- name: Cache_dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
id: cache
|
||||||
|
with:
|
||||||
|
path: ~/dependencies/
|
||||||
|
key: ${{ runner.os }}-dependencies
|
||||||
|
|
||||||
|
- name: pip cache (linux)
|
||||||
|
uses: actions/cache@v3
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
with:
|
||||||
|
path: ~/.cache/pip
|
||||||
|
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||||
|
|
||||||
|
- name: TA binary *nix
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
||||||
|
|
||||||
|
- name: Installation - *nix
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip wheel
|
||||||
|
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
||||||
|
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
||||||
|
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
||||||
|
pip install -r requirements-dev.txt
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
- name: Tests incl. ccxt compatibility tests
|
||||||
|
run: |
|
||||||
|
pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun
|
||||||
|
|
||||||
|
|
||||||
# Notify only once - when CI completes (and after deploy) in case it's successfull
|
# Notify only once - when CI completes (and after deploy) in case it's successfull
|
||||||
notify-complete:
|
notify-complete:
|
||||||
needs: [ build_linux, build_macos, build_windows, docs_check, mypy_version_check, pre-commit ]
|
needs: [
|
||||||
|
build_linux,
|
||||||
|
build_macos,
|
||||||
|
build_windows,
|
||||||
|
docs_check,
|
||||||
|
mypy_version_check,
|
||||||
|
pre-commit,
|
||||||
|
build_linux_online
|
||||||
|
]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
# Discord notification can't handle schedule events
|
# Discord notification can't handle schedule events
|
||||||
if: (github.event_name != 'schedule')
|
if: (github.event_name != 'schedule')
|
||||||
|
@ -15,9 +15,9 @@ repos:
|
|||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.2.1
|
- types-cachetools==5.2.1
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.28.11.4
|
- types-requests==2.28.11.5
|
||||||
- types-tabulate==0.9.0.0
|
- types-tabulate==0.9.0.0
|
||||||
- types-python-dateutil==2.8.19.3
|
- types-python-dateutil==2.8.19.4
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
|
@ -7,11 +7,13 @@ export DOCKER_BUILDKIT=1
|
|||||||
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
|
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
|
||||||
TAG_PLOT=${TAG}_plot
|
TAG_PLOT=${TAG}_plot
|
||||||
TAG_FREQAI=${TAG}_freqai
|
TAG_FREQAI=${TAG}_freqai
|
||||||
|
TAG_FREQAI_RL=${TAG_FREQAI}rl
|
||||||
TAG_PI="${TAG}_pi"
|
TAG_PI="${TAG}_pi"
|
||||||
|
|
||||||
TAG_ARM=${TAG}_arm
|
TAG_ARM=${TAG}_arm
|
||||||
TAG_PLOT_ARM=${TAG_PLOT}_arm
|
TAG_PLOT_ARM=${TAG_PLOT}_arm
|
||||||
TAG_FREQAI_ARM=${TAG_FREQAI}_arm
|
TAG_FREQAI_ARM=${TAG_FREQAI}_arm
|
||||||
|
TAG_FREQAI_RL_ARM=${TAG_FREQAI_RL}_arm
|
||||||
CACHE_IMAGE=freqtradeorg/freqtrade_cache
|
CACHE_IMAGE=freqtradeorg/freqtrade_cache
|
||||||
|
|
||||||
echo "Running for ${TAG}"
|
echo "Running for ${TAG}"
|
||||||
@ -41,9 +43,11 @@ docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM
|
|||||||
|
|
||||||
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 --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 --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 --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 --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 tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM
|
docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM
|
||||||
docker tag freqtrade:$TAG_FREQAI_ARM ${CACHE_IMAGE}:$TAG_FREQAI_ARM
|
docker tag freqtrade:$TAG_FREQAI_ARM ${CACHE_IMAGE}:$TAG_FREQAI_ARM
|
||||||
|
docker tag freqtrade:$TAG_FREQAI_RL_ARM ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM
|
||||||
|
|
||||||
# Run backtest
|
# Run backtest
|
||||||
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
|
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
|
||||||
@ -58,6 +62,7 @@ docker images
|
|||||||
# docker push ${IMAGE_NAME}
|
# docker push ${IMAGE_NAME}
|
||||||
docker push ${CACHE_IMAGE}:$TAG_PLOT_ARM
|
docker push ${CACHE_IMAGE}:$TAG_PLOT_ARM
|
||||||
docker push ${CACHE_IMAGE}:$TAG_FREQAI_ARM
|
docker push ${CACHE_IMAGE}:$TAG_FREQAI_ARM
|
||||||
|
docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM
|
||||||
docker push ${CACHE_IMAGE}:$TAG_ARM
|
docker push ${CACHE_IMAGE}:$TAG_ARM
|
||||||
|
|
||||||
# Create multi-arch image
|
# Create multi-arch image
|
||||||
@ -74,6 +79,9 @@ docker manifest push -p ${IMAGE_NAME}:${TAG_PLOT}
|
|||||||
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI_ARM} ${CACHE_IMAGE}:${TAG_FREQAI}
|
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI_ARM} ${CACHE_IMAGE}:${TAG_FREQAI}
|
||||||
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI}
|
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI}
|
||||||
|
|
||||||
|
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} ${CACHE_IMAGE}:${TAG_FREQAI_RL}
|
||||||
|
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL}
|
||||||
|
|
||||||
# Tag as latest for develop builds
|
# Tag as latest for develop builds
|
||||||
if [ "${TAG}" = "develop" ]; then
|
if [ "${TAG}" = "develop" ]; then
|
||||||
docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
|
docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
|
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
|
||||||
TAG_PLOT=${TAG}_plot
|
TAG_PLOT=${TAG}_plot
|
||||||
TAG_FREQAI=${TAG}_freqai
|
TAG_FREQAI=${TAG}_freqai
|
||||||
|
TAG_FREQAI_RL=${TAG_FREQAI}rl
|
||||||
TAG_PI="${TAG}_pi"
|
TAG_PI="${TAG}_pi"
|
||||||
|
|
||||||
PI_PLATFORM="linux/arm/v7"
|
PI_PLATFORM="linux/arm/v7"
|
||||||
@ -51,9 +52,11 @@ 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 --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --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 --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --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 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
|
||||||
|
docker tag freqtrade:$TAG_FREQAI_RL ${CACHE_IMAGE}:$TAG_FREQAI_RL
|
||||||
|
|
||||||
# Run backtest
|
# Run backtest
|
||||||
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
|
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
|
||||||
@ -68,6 +71,7 @@ docker images
|
|||||||
docker push ${CACHE_IMAGE}
|
docker push ${CACHE_IMAGE}
|
||||||
docker push ${CACHE_IMAGE}:$TAG_PLOT
|
docker push ${CACHE_IMAGE}:$TAG_PLOT
|
||||||
docker push ${CACHE_IMAGE}:$TAG_FREQAI
|
docker push ${CACHE_IMAGE}:$TAG_FREQAI
|
||||||
|
docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL
|
||||||
docker push ${CACHE_IMAGE}:$TAG
|
docker push ${CACHE_IMAGE}:$TAG
|
||||||
|
|
||||||
|
|
||||||
|
8
docker/Dockerfile.freqai_rl
Normal file
8
docker/Dockerfile.freqai_rl
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
ARG sourceimage=freqtradeorg/freqtrade
|
||||||
|
ARG sourcetag=develop_freqai
|
||||||
|
FROM ${sourceimage}:${sourcetag}
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
COPY requirements-freqai.txt requirements-freqai-rl.txt /freqtrade/
|
||||||
|
|
||||||
|
RUN pip install -r requirements-freqai-rl.txt --user --no-cache-dir
|
@ -100,3 +100,17 @@ freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 2 --enter-re
|
|||||||
The indicators have to be present in your strategy's main DataFrame (either for your main
|
The indicators have to be present in your strategy's main DataFrame (either for your main
|
||||||
timeframe or for informative timeframes) otherwise they will simply be ignored in the script
|
timeframe or for informative timeframes) otherwise they will simply be ignored in the script
|
||||||
output.
|
output.
|
||||||
|
|
||||||
|
### Filtering the trade output by date
|
||||||
|
|
||||||
|
To show only trades between dates within your backtested timerange, supply the usual `timerange` option in `YYYYMMDD-[YYYYMMDD]` format:
|
||||||
|
|
||||||
|
```
|
||||||
|
--timerange : Timerange to filter output trades, start date inclusive, end date exclusive. e.g. 20220101-20221231
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, if your backtest timerange was `20220101-20221231` but you only want to output trades in January:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
freqtrade backtesting-analysis -c <config.json> --timerange 20220101-20220201
|
||||||
|
```
|
||||||
|
@ -583,7 +583,8 @@ To utilize this, you can append `--timeframe-detail 5m` to your regular backtest
|
|||||||
freqtrade backtesting --strategy AwesomeStrategy --timeframe 1h --timeframe-detail 5m
|
freqtrade backtesting --strategy AwesomeStrategy --timeframe 1h --timeframe-detail 5m
|
||||||
```
|
```
|
||||||
|
|
||||||
This will load 1h data as well as 5m data for the timeframe. The strategy will be analyzed with the 1h timeframe - and for every "open trade candle" (candles where a trade is open) the 5m data will be used to simulate intra-candle movements.
|
This will load 1h data as well as 5m data for the timeframe. The strategy will be analyzed with the 1h timeframe, and Entry orders will only be placed at the main timeframe, however Order fills and exit signals will be evaluated at the 5m candle, simulating intra-candle movements.
|
||||||
|
|
||||||
All callback functions (`custom_exit()`, `custom_stoploss()`, ... ) will be running for each 5m candle once the trade is opened (so 12 times in the above example of 1h timeframe, and 5m detailed timeframe).
|
All callback functions (`custom_exit()`, `custom_stoploss()`, ... ) will be running for each 5m candle once the trade is opened (so 12 times in the above example of 1h timeframe, and 5m detailed timeframe).
|
||||||
|
|
||||||
`--timeframe-detail` must be smaller than the original timeframe, otherwise backtesting will fail to start.
|
`--timeframe-detail` must be smaller than the original timeframe, otherwise backtesting will fail to start.
|
||||||
|
@ -665,6 +665,7 @@ You should also make sure to read the [Exchanges](exchanges.md) section of the d
|
|||||||
### Using proxy with Freqtrade
|
### Using proxy with Freqtrade
|
||||||
|
|
||||||
To use a proxy with freqtrade, export your proxy settings using the variables `"HTTP_PROXY"` and `"HTTPS_PROXY"` set to the appropriate values.
|
To use a proxy with freqtrade, export your proxy settings using the variables `"HTTP_PROXY"` and `"HTTPS_PROXY"` set to the appropriate values.
|
||||||
|
This will have the proxy settings applied to everything (telegram, coingecko, ...) except exchange requests.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
export HTTP_PROXY="http://addr:port"
|
export HTTP_PROXY="http://addr:port"
|
||||||
@ -672,17 +673,20 @@ export HTTPS_PROXY="http://addr:port"
|
|||||||
freqtrade
|
freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Proxy just exchange requests
|
#### Proxy exchange requests
|
||||||
|
|
||||||
To use a proxy just for exchange connections (skips/ignores telegram and coingecko) - you can also define the proxies as part of the ccxt configuration.
|
To use a proxy for exchange connections - you will have to define the proxies as part of the ccxt configuration.
|
||||||
|
|
||||||
``` json
|
``` json
|
||||||
"ccxt_config": {
|
{
|
||||||
|
"exchange": {
|
||||||
|
"ccxt_config": {
|
||||||
"aiohttp_proxy": "http://addr:port",
|
"aiohttp_proxy": "http://addr:port",
|
||||||
"proxies": {
|
"proxies": {
|
||||||
"http": "http://addr:port",
|
"http": "http://addr:port",
|
||||||
"https": "http://addr:port"
|
"https": "http://addr:port"
|
||||||
},
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ from pathlib import Path
|
|||||||
project_root = "somedir/freqtrade"
|
project_root = "somedir/freqtrade"
|
||||||
i=0
|
i=0
|
||||||
try:
|
try:
|
||||||
os.chdirdir(project_root)
|
os.chdir(project_root)
|
||||||
assert Path('LICENSE').is_file()
|
assert Path('LICENSE').is_file()
|
||||||
except:
|
except:
|
||||||
while i<4 and (not Path('LICENSE').is_file()):
|
while i<4 and (not Path('LICENSE').is_file()):
|
||||||
|
@ -49,6 +49,13 @@ For more information about the [Remote container extension](https://code.visuals
|
|||||||
New code should be covered by basic unittests. Depending on the complexity of the feature, Reviewers may request more in-depth unittests.
|
New code should be covered by basic unittests. Depending on the complexity of the feature, Reviewers may request more in-depth unittests.
|
||||||
If necessary, the Freqtrade team can assist and give guidance with writing good tests (however please don't expect anyone to write the tests for you).
|
If necessary, the Freqtrade team can assist and give guidance with writing good tests (however please don't expect anyone to write the tests for you).
|
||||||
|
|
||||||
|
#### How to run tests
|
||||||
|
|
||||||
|
Use `pytest` in root folder to run all available testcases and confirm your local environment is setup correctly
|
||||||
|
|
||||||
|
!!! Note "feature branches"
|
||||||
|
Tests are expected to pass on the `develop` and `stable` branches. Other branches may be work in progress with tests not working yet.
|
||||||
|
|
||||||
#### Checking log content in tests
|
#### Checking log content in tests
|
||||||
|
|
||||||
Freqtrade uses 2 main methods to check log content in tests, `log_has()` and `log_has_re()` (to check using regex, in case of dynamic log-messages).
|
Freqtrade uses 2 main methods to check log content in tests, `log_has()` and `log_has_re()` (to check using regex, in case of dynamic log-messages).
|
||||||
@ -434,6 +441,11 @@ To keep the release-log short, best wrap the full git changelog into a collapsib
|
|||||||
</details>
|
</details>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### FreqUI release
|
||||||
|
|
||||||
|
If FreqUI has been updated substantially, make sure to create a release before merging the release branch.
|
||||||
|
Make sure that freqUI CI on the release is finished and passed before merging the release.
|
||||||
|
|
||||||
### Create github release / tag
|
### Create github release / tag
|
||||||
|
|
||||||
Once the PR against stable is merged (best right after merging):
|
Once the PR against stable is merged (best right after merging):
|
||||||
|
@ -4,9 +4,11 @@ The table below will list all configuration parameters available for FreqAI. Som
|
|||||||
|
|
||||||
Mandatory parameters are marked as **Required** and have to be set in one of the suggested ways.
|
Mandatory parameters are marked as **Required** and have to be set in one of the suggested ways.
|
||||||
|
|
||||||
|
### General configuration parameters
|
||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
|------------|-------------|
|
|------------|-------------|
|
||||||
| | **General configuration parameters**
|
| | **General configuration parameters within the `config.freqai` tree**
|
||||||
| `freqai` | **Required.** <br> The parent dictionary containing all the parameters for controlling FreqAI. <br> **Datatype:** Dictionary.
|
| `freqai` | **Required.** <br> The parent dictionary containing all the parameters for controlling FreqAI. <br> **Datatype:** Dictionary.
|
||||||
| `train_period_days` | **Required.** <br> Number of days to use for the training data (width of the sliding window). <br> **Datatype:** Positive integer.
|
| `train_period_days` | **Required.** <br> Number of days to use for the training data (width of the sliding window). <br> **Datatype:** Positive integer.
|
||||||
| `backtest_period_days` | **Required.** <br> Number of days to inference from the trained model before sliding the `train_period_days` window defined above, and retraining the model during backtesting (more info [here](freqai-running.md#backtesting)). This can be fractional days, but beware that the provided `timerange` will be divided by this number to yield the number of trainings necessary to complete the backtest. <br> **Datatype:** Float.
|
| `backtest_period_days` | **Required.** <br> Number of days to inference from the trained model before sliding the `train_period_days` window defined above, and retraining the model during backtesting (more info [here](freqai-running.md#backtesting)). This can be fractional days, but beware that the provided `timerange` will be divided by this number to yield the number of trainings necessary to complete the backtest. <br> **Datatype:** Float.
|
||||||
@ -19,7 +21,13 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| `follow_mode` | Use a `follower` that will look for models associated with a specific `identifier` and load those for inferencing. A `follower` will **not** train new models. <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `follow_mode` | Use a `follower` that will look for models associated with a specific `identifier` and load those for inferencing. A `follower` will **not** train new models. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
| `continual_learning` | Use the final state of the most recently trained model as starting point for the new model, allowing for incremental learning (more information can be found [here](freqai-running.md#continual-learning)). <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `continual_learning` | Use the final state of the most recently trained model as starting point for the new model, allowing for incremental learning (more information can be found [here](freqai-running.md#continual-learning)). <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
| `write_metrics_to_disk` | Collect train timings, inference timings and cpu usage in json file. <br> **Datatype:** Boolean. <br> Default: `False`
|
| `write_metrics_to_disk` | Collect train timings, inference timings and cpu usage in json file. <br> **Datatype:** Boolean. <br> Default: `False`
|
||||||
| | **Feature parameters**
|
| `data_kitchen_thread_count` | <br> Designate the number of threads you want to use for data processing (outlier methods, normalization, etc.). This has no impact on the number of threads used for training. If user does not set it (default), FreqAI will use max number of threads - 2 (leaving 1 physical core available for Freqtrade bot and FreqUI) <br> **Datatype:** Positive integer.
|
||||||
|
|
||||||
|
### Feature parameters
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| | **Feature parameters within the `freqai.feature_parameters` sub dictionary**
|
||||||
| `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples are shown [here](freqai-feature-engineering.md). <br> **Datatype:** Dictionary.
|
| `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples are shown [here](freqai-feature-engineering.md). <br> **Datatype:** Dictionary.
|
||||||
| `include_timeframes` | A list of timeframes that all indicators in `populate_any_indicators` will be created for. The list is added as features to the base indicators dataset. <br> **Datatype:** List of timeframes (strings).
|
| `include_timeframes` | A list of timeframes that all indicators in `populate_any_indicators` will be created for. The list is added as features to the base indicators dataset. <br> **Datatype:** List of timeframes (strings).
|
||||||
| `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `populate_any_indicators` during feature engineering (see details [here](freqai-feature-engineering.md)) will be created for each correlated coin. The correlated coins features are added to the base indicators dataset. <br> **Datatype:** List of assets (strings).
|
| `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `populate_any_indicators` during feature engineering (see details [here](freqai-feature-engineering.md)) will be created for each correlated coin. The correlated coins features are added to the base indicators dataset. <br> **Datatype:** List of assets (strings).
|
||||||
@ -38,16 +46,49 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| `noise_standard_deviation` | If set, FreqAI adds noise to the training features with the aim of preventing overfitting. FreqAI generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. `noise_standard_deviation` should be kept relative to the normalized space, i.e., between -1 and 1. In other words, since data in FreqAI is always normalized to be between -1 and 1, `noise_standard_deviation: 0.05` would result in 32% of the data being randomly increased/decreased by more than 2.5% (i.e., the percent of data falling within the first standard deviation). <br> **Datatype:** Integer. <br> Default: `0`.
|
| `noise_standard_deviation` | If set, FreqAI adds noise to the training features with the aim of preventing overfitting. FreqAI generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. `noise_standard_deviation` should be kept relative to the normalized space, i.e., between -1 and 1. In other words, since data in FreqAI is always normalized to be between -1 and 1, `noise_standard_deviation: 0.05` would result in 32% of the data being randomly increased/decreased by more than 2.5% (i.e., the percent of data falling within the first standard deviation). <br> **Datatype:** Integer. <br> Default: `0`.
|
||||||
| `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset. <br> **Datatype:** Float. <br> Default: `30`.
|
| `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).
|
||||||
| | **Data split parameters**
|
|
||||||
|
### Data split parameters
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| | **Data split parameters within the `freqai.data_split_parameters` sub dictionary**
|
||||||
| `data_split_parameters` | Include any additional parameters available from Scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). <br> **Datatype:** Dictionary.
|
| `data_split_parameters` | Include any additional parameters available from Scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). <br> **Datatype:** Dictionary.
|
||||||
| `test_size` | The fraction of data that should be used for testing instead of training. <br> **Datatype:** Positive float < 1.
|
| `test_size` | The fraction of data that should be used for testing instead of training. <br> **Datatype:** Positive float < 1.
|
||||||
| `shuffle` | Shuffle the training data points during training. Typically, to not remove the chronological order of data in time-series forecasting, this is set to `False`. <br> **Datatype:** Boolean. <br> Defaut: `False`.
|
| `shuffle` | Shuffle the training data points during training. Typically, to not remove the chronological order of data in time-series forecasting, this is set to `False`. <br> **Datatype:** Boolean. <br> Defaut: `False`.
|
||||||
| | **Model training parameters**
|
|
||||||
|
### Model training parameters
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| | **Model training parameters within the `freqai.model_training_parameters` sub dictionary**
|
||||||
| `model_training_parameters` | A flexible dictionary that includes all parameters available by the selected model library. For example, if you use `LightGBMRegressor`, this dictionary can contain any parameter available by the `LightGBMRegressor` [here](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.LGBMRegressor.html) (external website). If you select a different model, this dictionary can contain any parameter from that model. A list of the currently available models can be found [here](freqai-configuration.md#using-different-prediction-models). <br> **Datatype:** Dictionary.
|
| `model_training_parameters` | A flexible dictionary that includes all parameters available by the selected model library. For example, if you use `LightGBMRegressor`, this dictionary can contain any parameter available by the `LightGBMRegressor` [here](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.LGBMRegressor.html) (external website). If you select a different model, this dictionary can contain any parameter from that model. A list of the currently available models can be found [here](freqai-configuration.md#using-different-prediction-models). <br> **Datatype:** Dictionary.
|
||||||
| `n_estimators` | The number of boosted trees to fit in the training of the model. <br> **Datatype:** Integer.
|
| `n_estimators` | The number of boosted trees to fit in the training of the model. <br> **Datatype:** Integer.
|
||||||
| `learning_rate` | Boosting learning rate during training of the model. <br> **Datatype:** Float.
|
| `learning_rate` | Boosting learning rate during training of the model. <br> **Datatype:** Float.
|
||||||
| `n_jobs`, `thread_count`, `task_type` | Set the number of threads for parallel processing and the `task_type` (`gpu` or `cpu`). Different model libraries use different parameter names. <br> **Datatype:** Float.
|
| `n_jobs`, `thread_count`, `task_type` | Set the number of threads for parallel processing and the `task_type` (`gpu` or `cpu`). Different model libraries use different parameter names. <br> **Datatype:** Float.
|
||||||
|
|
||||||
|
### Reinforcement Learning parameters
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| | **Reinforcement Learning Parameters within the `freqai.rl_config` sub dictionary**
|
||||||
|
| `rl_config` | A dictionary containing the control parameters for a Reinforcement Learning model. <br> **Datatype:** Dictionary.
|
||||||
|
| `train_cycles` | Training time steps will be set based on the `train_cycles * number of training data points. <br> **Datatype:** Integer.
|
||||||
|
| `cpu_count` | Number of processors to dedicate to the Reinforcement Learning training process. <br> **Datatype:** int.
|
||||||
|
| `max_trade_duration_candles`| Guides the agent training to keep trades below desired length. Example usage shown in `prediction_models/ReinforcementLearner.py` within the customizable `calculate_reward()` function. <br> **Datatype:** int.
|
||||||
|
| `model_type` | Model string from stable_baselines3 or SBcontrib. Available strings include: `'TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO', 'PPO', 'A2C', 'DQN'`. User should ensure that `model_training_parameters` match those available to the corresponding stable_baselines3 model by visiting their documentaiton. [PPO doc](https://stable-baselines3.readthedocs.io/en/master/modules/ppo.html) (external website) <br> **Datatype:** string.
|
||||||
|
| `policy_type` | One of the available policy types from stable_baselines3 <br> **Datatype:** string.
|
||||||
|
| `max_training_drawdown_pct` | The maximum drawdown that the agent is allowed to experience during training. <br> **Datatype:** float. <br> Default: 0.8
|
||||||
|
| `cpu_count` | Number of threads/cpus to dedicate to the Reinforcement Learning training process (depending on if `ReinforcementLearning_multiproc` is selected or not). Recommended to leave this untouched, by default, this value is set to the total number of physical cores minus 1. <br> **Datatype:** int.
|
||||||
|
| `model_reward_parameters` | Parameters used inside the customizable `calculate_reward()` function in `ReinforcementLearner.py` <br> **Datatype:** int.
|
||||||
|
| `add_state_info` | Tell FreqAI to include state information in the feature set for training and inferencing. The current state variables include trade duration, current profit, trade position. This is only available in dry/live runs, and is automatically switched to false for backtesting. <br> **Datatype:** bool. <br> Default: `False`.
|
||||||
|
| `net_arch` | Network architecture which is well described in [`stable_baselines3` doc](https://stable-baselines3.readthedocs.io/en/master/guide/custom_policy.html#examples). In summary: `[<shared layers>, dict(vf=[<non-shared value network layers>], pi=[<non-shared policy network layers>])]`. By default this is set to `[128, 128]`, which defines 2 shared hidden layers with 128 units each.
|
||||||
|
| `randomize_starting_position` | Randomize the starting point of each episode to avoid overfitting. <br> **Datatype:** bool. <br> Default: `False`.
|
||||||
|
|
||||||
|
### Additional parameters
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|------------|-------------|
|
||||||
| | **Extraneous parameters**
|
| | **Extraneous parameters**
|
||||||
| `keras` | If the selected model makes use of Keras (typical for Tensorflow-based prediction models), this flag needs to be activated so that the model save/loading follows Keras standards. <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `freqai.keras` | If the selected model makes use of Keras (typical for Tensorflow-based prediction models), this flag needs to be activated so that the model save/loading follows Keras standards. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
| `conv_width` | The width of a convolutional neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. <br> **Datatype:** Integer. <br> Default: `2`.
|
| `freqai.conv_width` | The width of a convolutional neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. <br> **Datatype:** Integer. <br> Default: `2`.
|
||||||
| `reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage and decreasing train/inference timing. This parameter is set in the main level of the Freqtrade configuration file (not inside FreqAI). <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `freqai.reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage and decreasing train/inference timing. This parameter is set in the main level of the Freqtrade configuration file (not inside FreqAI). <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
|
260
docs/freqai-reinforcement-learning.md
Normal file
260
docs/freqai-reinforcement-learning.md
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
# Reinforcement Learning
|
||||||
|
|
||||||
|
!!! Note "Installation size"
|
||||||
|
Reinforcement learning dependencies include large packages such as `torch`, which should be explicitly requested during `./setup.sh -i` by answering "y" to the question "Do you also want dependencies for freqai-rl (~700mb additional space required) [y/N]?".
|
||||||
|
Users who prefer docker should ensure they use the docker image appended with `_freqairl`.
|
||||||
|
|
||||||
|
## Background and terminology
|
||||||
|
|
||||||
|
### What is RL and why does FreqAI need it?
|
||||||
|
|
||||||
|
Reinforcement learning involves two important components, the *agent* and the training *environment*. During agent training, the agent moves through historical data candle by candle, always making 1 of a set of actions: Long entry, long exit, short entry, short exit, neutral). During this training process, the environment tracks the performance of these actions and rewards the agent according to a custom user made `calculate_reward()` (here we offer a default reward for users to build on if they wish [details here](#creating-a-custom-reward-function)). The reward is used to train weights in a neural network.
|
||||||
|
|
||||||
|
A second important component of the FreqAI RL implementation is the use of *state* information. State information is fed into the network at each step, including current profit, current position, and current trade duration. These are used to train the agent in the training environment, and to reinforce the agent in dry/live (this functionality is not available in backtesting). *FreqAI + Freqtrade is a perfect match for this reinforcing mechanism since this information is readily available in live deployments.*
|
||||||
|
|
||||||
|
Reinforcement learning is a natural progression for FreqAI, since it adds a new layer of adaptivity and market reactivity that Classifiers and Regressors cannot match. However, Classifiers and Regressors have strengths that RL does not have such as robust predictions. Improperly trained RL agents may find "cheats" and "tricks" to maximize reward without actually winning any trades. For this reason, RL is more complex and demands a higher level of understanding than typical Classifiers and Regressors.
|
||||||
|
|
||||||
|
### The RL interface
|
||||||
|
|
||||||
|
With the current framework, we aim to expose the training environment via the common "prediction model" file, which is a user inherited `BaseReinforcementLearner` object (e.g. `freqai/prediction_models/ReinforcementLearner`). Inside this user class, the RL environment is available and customized via `MyRLEnv` as [shown below](#creating-a-custom-reward-function).
|
||||||
|
|
||||||
|
We envision the majority of users focusing their effort on creative design of the `calculate_reward()` function [details here](#creating-a-custom-reward-function), while leaving the rest of the environment untouched. Other users may not touch the environment at all, and they will only play with the configuration settings and the powerful feature engineering that already exists in FreqAI. Meanwhile, we enable advanced users to create their own model classes entirely.
|
||||||
|
|
||||||
|
The framework is built on stable_baselines3 (torch) and OpenAI gym for the base environment class. But generally speaking, the model class is well isolated. Thus, the addition of competing libraries can be easily integrated into the existing framework. For the environment, it is inheriting from `gym.env` which means that it is necessary to write an entirely new environment in order to switch to a different library.
|
||||||
|
|
||||||
|
### Important considerations
|
||||||
|
|
||||||
|
As explained above, the agent is "trained" in an artificial trading "environment". In our case, that environment may seem quite similar to a real Freqtrade backtesting environment, but it is *NOT*. In fact, the RL training environment is much more simplified. It does not incorporate any of the complicated strategy logic, such as callbacks like `custom_exit`, `custom_stoploss`, leverage controls, etc. The RL environment is instead a very "raw" representation of the true market, where the agent has free-will to learn the policy (read: stoploss, take profit, etc.) which is enforced by the `calculate_reward()`. Thus, it is important to consider that the agent training environment is not identical to the real world.
|
||||||
|
|
||||||
|
## Running Reinforcement Learning
|
||||||
|
|
||||||
|
Setting up and running a Reinforcement Learning model is the same as running a Regressor or Classifier. The same two flags, `--freqaimodel` and `--strategy`, must be defined on the command line:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
freqtrade trade --freqaimodel ReinforcementLearner --strategy MyRLStrategy --config config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
where `ReinforcementLearner` will use the templated `ReinforcementLearner` from `freqai/prediction_models/ReinforcementLearner` (or a custom user defined one located in `user_data/freqaimodels`). The strategy, on the other hand, follows the same base [feature engineering](freqai-feature-engineering.md) with `populate_any_indicators` as a typical Regressor:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def populate_any_indicators(
|
||||||
|
self, pair, df, tf, informative=None, set_generalized_indicators=False
|
||||||
|
):
|
||||||
|
|
||||||
|
if informative is None:
|
||||||
|
informative = self.dp.get_pair_dataframe(pair, tf)
|
||||||
|
|
||||||
|
# first loop is automatically duplicating indicators for time periods
|
||||||
|
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
|
||||||
|
|
||||||
|
t = int(t)
|
||||||
|
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
|
||||||
|
informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
|
||||||
|
informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, window=t)
|
||||||
|
|
||||||
|
# The following raw price values are necessary for RL models
|
||||||
|
informative[f"%-{pair}raw_close"] = informative["close"]
|
||||||
|
informative[f"%-{pair}raw_open"] = informative["open"]
|
||||||
|
informative[f"%-{pair}raw_high"] = informative["high"]
|
||||||
|
informative[f"%-{pair}raw_low"] = informative["low"]
|
||||||
|
|
||||||
|
indicators = [col for col in informative if col.startswith("%")]
|
||||||
|
# This loop duplicates and shifts all indicators to add a sense of recency to data
|
||||||
|
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
|
||||||
|
if n == 0:
|
||||||
|
continue
|
||||||
|
informative_shift = informative[indicators].shift(n)
|
||||||
|
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
|
||||||
|
informative = pd.concat((informative, informative_shift), axis=1)
|
||||||
|
|
||||||
|
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
|
||||||
|
skip_columns = [
|
||||||
|
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
|
||||||
|
]
|
||||||
|
df = df.drop(columns=skip_columns)
|
||||||
|
|
||||||
|
# Add generalized indicators here (because in live, it will call this
|
||||||
|
# function to populate indicators during training). Notice how we ensure not to
|
||||||
|
# add them multiple times
|
||||||
|
if set_generalized_indicators:
|
||||||
|
|
||||||
|
# For RL, there are no direct targets to set. This is filler (neutral)
|
||||||
|
# until the agent sends an action.
|
||||||
|
df["&-action"] = 0
|
||||||
|
|
||||||
|
return df
|
||||||
|
```
|
||||||
|
|
||||||
|
Most of the function remains the same as for typical Regressors, however, the function above shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# The following features are necessary for RL models
|
||||||
|
informative[f"%-{pair}raw_close"] = informative["close"]
|
||||||
|
informative[f"%-{pair}raw_open"] = informative["open"]
|
||||||
|
informative[f"%-{pair}raw_high"] = informative["high"]
|
||||||
|
informative[f"%-{pair}raw_low"] = informative["low"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, there is no explicit "label" to make - instead it is necessary to assign the `&-action` column which will contain the agent's actions when accessed in `populate_entry/exit_trends()`. In the present example, the neutral action to 0. This value should align with the environment used. FreqAI provides two environments, both use 0 as the neutral action.
|
||||||
|
|
||||||
|
After users realize there are no labels to set, they will soon understand that the agent is making its "own" entry and exit decisions. This makes strategy construction rather simple. The entry and exit signals come from the agent in the form of an integer - which are used directly to decide entries and exits in the strategy:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
|
||||||
|
enter_long_conditions = [df["do_predict"] == 1, df["&-action"] == 1]
|
||||||
|
|
||||||
|
if enter_long_conditions:
|
||||||
|
df.loc[
|
||||||
|
reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"]
|
||||||
|
] = (1, "long")
|
||||||
|
|
||||||
|
enter_short_conditions = [df["do_predict"] == 1, df["&-action"] == 3]
|
||||||
|
|
||||||
|
if enter_short_conditions:
|
||||||
|
df.loc[
|
||||||
|
reduce(lambda x, y: x & y, enter_short_conditions), ["enter_short", "enter_tag"]
|
||||||
|
] = (1, "short")
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
exit_long_conditions = [df["do_predict"] == 1, df["&-action"] == 2]
|
||||||
|
if exit_long_conditions:
|
||||||
|
df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1
|
||||||
|
|
||||||
|
exit_short_conditions = [df["do_predict"] == 1, df["&-action"] == 4]
|
||||||
|
if exit_short_conditions:
|
||||||
|
df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1
|
||||||
|
|
||||||
|
return df
|
||||||
|
```
|
||||||
|
|
||||||
|
It is important to consider that `&-action` depends on which environment they choose to use. The example above shows 5 actions, where 0 is neutral, 1 is enter long, 2 is exit long, 3 is enter short and 4 is exit short.
|
||||||
|
|
||||||
|
## Configuring the Reinforcement Learner
|
||||||
|
|
||||||
|
In order to configure the `Reinforcement Learner` the following dictionary must exist in the `freqai` config:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"rl_config": {
|
||||||
|
"train_cycles": 25,
|
||||||
|
"add_state_info": true,
|
||||||
|
"max_trade_duration_candles": 300,
|
||||||
|
"max_training_drawdown_pct": 0.02,
|
||||||
|
"cpu_count": 8,
|
||||||
|
"model_type": "PPO",
|
||||||
|
"policy_type": "MlpPolicy",
|
||||||
|
"model_reward_parameters": {
|
||||||
|
"rr": 1,
|
||||||
|
"profit_aim": 0.025
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Parameter details can be found [here](freqai-parameter-table.md), but in general the `train_cycles` decides how many times the agent should cycle through the candle data in its artificial environment to train weights in the model. `model_type` is a string which selects one of the available models in [stable_baselines](https://stable-baselines3.readthedocs.io/en/master/)(external link).
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
If you would like to experiment with `continual_learning`, then you should set that value to `true` in the main `freqai` configuration dictionary. This will tell the Reinforcement Learning library to continue training new models from the final state of previous models, instead of retraining new models from scratch each time a retrain is initiated.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Remember that the general `model_training_parameters` dictionary should contain all the model hyperparameter customizations for the particular `model_type`. For example, `PPO` parameters can be found [here](https://stable-baselines3.readthedocs.io/en/master/modules/ppo.html).
|
||||||
|
|
||||||
|
## Creating a custom reward function
|
||||||
|
|
||||||
|
As you begin to modify the strategy and the prediction model, you will quickly realize some important differences between the Reinforcement Learner and the Regressors/Classifiers. Firstly, the strategy does not set a target value (no labels!). Instead, you set the `calculate_reward()` function inside the `MyRLEnv` class (see below). A default `calculate_reward()` is provided inside `prediction_models/ReinforcementLearner.py` to demonstrate the necessary building blocks for creating rewards, but users are encouraged to create their own custom reinforcement learning model class (see below) and save it to `user_data/freqaimodels`. It is inside the `calculate_reward()` where creative theories about the market can be expressed. For example, you can reward your agent when it makes a winning trade, and penalize the agent when it makes a losing trade. Or perhaps, you wish to reward the agent for entering trades, and penalize the agent for sitting in trades too long. Below we show examples of how these rewards are all calculated:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner
|
||||||
|
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions
|
||||||
|
|
||||||
|
|
||||||
|
class MyCoolRLModel(ReinforcementLearner):
|
||||||
|
"""
|
||||||
|
User created RL prediction model.
|
||||||
|
|
||||||
|
Save this file to `freqtrade/user_data/freqaimodels`
|
||||||
|
|
||||||
|
then use it with:
|
||||||
|
|
||||||
|
freqtrade trade --freqaimodel MyCoolRLModel --config config.json --strategy SomeCoolStrat
|
||||||
|
|
||||||
|
Here the users can override any of the functions
|
||||||
|
available in the `IFreqaiModel` inheritance tree. Most importantly for RL, this
|
||||||
|
is where the user overrides `MyRLEnv` (see below), to define custom
|
||||||
|
`calculate_reward()` function, or to override any other parts of the environment.
|
||||||
|
|
||||||
|
This class also allows users to override any other part of the IFreqaiModel tree.
|
||||||
|
For example, the user can override `def fit()` or `def train()` or `def predict()`
|
||||||
|
to take fine-tuned control over these processes.
|
||||||
|
|
||||||
|
Another common override may be `def data_cleaning_predict()` where the user can
|
||||||
|
take fine-tuned control over the data handling pipeline.
|
||||||
|
"""
|
||||||
|
class MyRLEnv(Base5ActionRLEnv):
|
||||||
|
"""
|
||||||
|
User made custom environment. This class inherits from BaseEnvironment and gym.env.
|
||||||
|
Users can override any functions from those parent classes. Here is an example
|
||||||
|
of a user customized `calculate_reward()` function.
|
||||||
|
"""
|
||||||
|
def calculate_reward(self, action: int) -> float:
|
||||||
|
# first, penalize if the action is not valid
|
||||||
|
if not self._is_valid(action):
|
||||||
|
return -2
|
||||||
|
pnl = self.get_unrealized_profit()
|
||||||
|
|
||||||
|
factor = 100
|
||||||
|
# reward agent for entering trades
|
||||||
|
if action in (Actions.Long_enter.value, Actions.Short_enter.value) \
|
||||||
|
and self._position == Positions.Neutral:
|
||||||
|
return 25
|
||||||
|
# discourage agent from not entering trades
|
||||||
|
if action == Actions.Neutral.value and self._position == Positions.Neutral:
|
||||||
|
return -1
|
||||||
|
max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
|
||||||
|
trade_duration = self._current_tick - self._last_trade_tick
|
||||||
|
if trade_duration <= max_trade_duration:
|
||||||
|
factor *= 1.5
|
||||||
|
elif trade_duration > max_trade_duration:
|
||||||
|
factor *= 0.5
|
||||||
|
# discourage sitting in position
|
||||||
|
if self._position in (Positions.Short, Positions.Long) and \
|
||||||
|
action == Actions.Neutral.value:
|
||||||
|
return -1 * trade_duration / max_trade_duration
|
||||||
|
# close long
|
||||||
|
if action == Actions.Long_exit.value and self._position == Positions.Long:
|
||||||
|
if pnl > self.profit_aim * self.rr:
|
||||||
|
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||||
|
return float(pnl * factor)
|
||||||
|
# close short
|
||||||
|
if action == Actions.Short_exit.value and self._position == Positions.Short:
|
||||||
|
if pnl > self.profit_aim * self.rr:
|
||||||
|
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||||
|
return float(pnl * factor)
|
||||||
|
return 0.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Tensorboard
|
||||||
|
|
||||||
|
Reinforcement Learning models benefit from tracking training metrics. FreqAI has integrated Tensorboard to allow users to track training and evaluation performance across all coins and across all retrainings. Tensorboard is activated via the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd freqtrade
|
||||||
|
tensorboard --logdir user_data/models/unique-id
|
||||||
|
```
|
||||||
|
|
||||||
|
where `unique-id` is the `identifier` set in the `freqai` configuration file. This command must be run in a separate shell to view the output in their browser at 127.0.0.1:6060 (6060 is the default port used by Tensorboard).
|
||||||
|
|
||||||
|
![tensorboard](assets/tensorboard.jpg)
|
||||||
|
|
||||||
|
### Choosing a base environment
|
||||||
|
|
||||||
|
FreqAI provides two base environments, `Base4ActionEnvironment` and `Base5ActionEnvironment`. As the names imply, the environments are customized for agents that can select from 4 or 5 actions. In the `Base4ActionEnvironment`, the agent can enter long, enter short, hold neutral, or exit position. Meanwhile, in the `Base5ActionEnvironment`, the agent has the same actions as Base4, but instead of a single exit action, it separates exit long and exit short. The main changes stemming from the environment selection include:
|
||||||
|
|
||||||
|
* the actions available in the `calculate_reward`
|
||||||
|
* the actions consumed by the user strategy
|
||||||
|
|
||||||
|
Both of the FreqAI provided environments inherit from an action/position agnostic environment object called the `BaseEnvironment`, which contains all shared logic. The architecture is designed to be easily customized. The simplest customization is the `calculate_reward()` (see details [here](#creating-a-custom-reward-function)). However, the customizations can be further extended into any of the functions inside the environment. You can do this by simply overriding those functions inside your `MyRLEnv` in the prediction model file. Or for more advanced customizations, it is encouraged to create an entirely new environment inherited from `BaseEnvironment`.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
FreqAI does not provide by default, a long-only training environment. However, creating one should be as simple as copy-pasting one of the built in environments and removing the `short` actions (and all associated references to those).
|
@ -79,16 +79,11 @@ To change your **features**, you **must** set a new `identifier` in the config t
|
|||||||
|
|
||||||
To save the models generated during a particular backtest so that you can start a live deployment from one of them instead of training a new model, you must set `save_backtest_models` to `True` in the config.
|
To save the models generated during a particular backtest so that you can start a live deployment from one of them instead of training a new model, you must set `save_backtest_models` to `True` in the config.
|
||||||
|
|
||||||
### Backtest live models
|
### Backtest live collected predictions
|
||||||
|
|
||||||
FreqAI allow you to reuse ready models through the backtest parameter `--freqai-backtest-live-models`. This can be useful when you want to reuse models generated in dry/run for comparison or other study. For that, you must set `"purge_old_models"` to `True` in the config.
|
FreqAI allow you to reuse live historic predictions through the backtest parameter `--freqai-backtest-live-models`. This can be useful when you want to reuse predictions generated in dry/run for comparison or other study.
|
||||||
|
|
||||||
The `--timerange` parameter must not be informed, as it will be automatically calculated through the training end dates of the models.
|
The `--timerange` parameter must not be informed, as it will be automatically calculated through the data in the historic predictions file.
|
||||||
|
|
||||||
Each model has an identifier derived from the training end date. If you have only 1 model trained, FreqAI will backtest from the training end date until the current date. If you have more than 1 model, each model will perform the backtesting according to the training end date until the training end date of the next model and so on. For the last model, the period of the previous model will be used for the execution.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Currently, there is no checking for expired models, even if the `expired_hours` parameter is set.
|
|
||||||
|
|
||||||
|
|
||||||
### Downloading data to cover the full backtest period
|
### Downloading data to cover the full backtest period
|
||||||
|
@ -21,6 +21,7 @@ Enable subscribing to an instance by adding the `external_message_consumer` sect
|
|||||||
"name": "default", // This can be any name you'd like, default is "default"
|
"name": "default", // This can be any name you'd like, default is "default"
|
||||||
"host": "127.0.0.1", // The host from your producer's api_server config
|
"host": "127.0.0.1", // The host from your producer's api_server config
|
||||||
"port": 8080, // The port from your producer's api_server config
|
"port": 8080, // The port from your producer's api_server config
|
||||||
|
"secure": false, // Use a secure websockets connection, default false
|
||||||
"ws_token": "sercet_Ws_t0ken" // The ws_token from your producer's api_server config
|
"ws_token": "sercet_Ws_t0ken" // The ws_token from your producer's api_server config
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -42,6 +43,7 @@ Enable subscribing to an instance by adding the `external_message_consumer` sect
|
|||||||
| `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> **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.
|
||||||
|
@ -389,6 +389,44 @@ Now anytime those types of RPC messages are sent in the bot, you will receive th
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Reverse Proxy setup
|
||||||
|
|
||||||
|
When using [Nginx](https://nginx.org/en/docs/), the following configuration is required for WebSockets to work (Note this configuration is incomplete, it's missing some information and can not be used as is):
|
||||||
|
|
||||||
|
Please make sure to replace `<freqtrade_listen_ip>` (and the subsequent port) with the IP and Port matching your configuration/setup.
|
||||||
|
|
||||||
|
```
|
||||||
|
http {
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
#...
|
||||||
|
|
||||||
|
server {
|
||||||
|
#...
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_pass http://<freqtrade_listen_ip>:8080;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To properly configure your reverse proxy (securely), please consult it's documentation for proxying websockets.
|
||||||
|
|
||||||
|
- **Traefik**: Traefik supports websockets out of the box, see the [documentation](https://doc.traefik.io/traefik/)
|
||||||
|
- **Caddy**: Caddy v2 supports websockets out of the box, see the [documentation](https://caddyserver.com/docs/v2-upgrade#proxy)
|
||||||
|
|
||||||
|
!!! Tip "SSL certificates"
|
||||||
|
You can use tools like certbot to setup ssl certificates to access your bot's UI through encrypted connection by using any fo the above reverse proxies.
|
||||||
|
While this will protect your data in transit, we do not recommend to run the freqtrade API outside of your private network (VPN, SSH tunnel).
|
||||||
|
|
||||||
### OpenAPI interface
|
### OpenAPI interface
|
||||||
|
|
||||||
To enable the builtin openAPI interface (Swagger UI), specify `"enable_openapi": true` in the api_server configuration.
|
To enable the builtin openAPI interface (Swagger UI), specify `"enable_openapi": true` in the api_server configuration.
|
||||||
|
@ -446,15 +446,17 @@ A full sample can be found [in the DataProvider section](#complete-data-provider
|
|||||||
|
|
||||||
??? Note "Alternative candle types"
|
??? Note "Alternative candle types"
|
||||||
Informative_pairs can also provide a 3rd tuple element defining the candle type explicitly.
|
Informative_pairs can also provide a 3rd tuple element defining the candle type explicitly.
|
||||||
Availability of alternative candle-types will depend on the trading-mode and the exchange. Details about this can be found in the exchange documentation.
|
Availability of alternative candle-types will depend on the trading-mode and the exchange.
|
||||||
|
In general, spot pairs cannot be used in futures markets, and futures candles can't be used as informative pairs for spot bots.
|
||||||
|
Details about this may vary, if they do, this can be found in the exchange documentation.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
def informative_pairs(self):
|
def informative_pairs(self):
|
||||||
return [
|
return [
|
||||||
("ETH/USDT", "5m", ""), # Uses default candletype, depends on trading_mode
|
("ETH/USDT", "5m", ""), # Uses default candletype, depends on trading_mode (recommended)
|
||||||
("ETH/USDT", "5m", "spot"), # Forces usage of spot candles
|
("ETH/USDT", "5m", "spot"), # Forces usage of spot candles (only valid for bots running on spot markets).
|
||||||
("BTC/TUSD", "15m", "futures"), # Uses futures candles
|
("BTC/TUSD", "15m", "futures"), # Uses futures candles (only bots with `trading_mode=futures`)
|
||||||
("BTC/TUSD", "15m", "mark"), # Uses mark candles
|
("BTC/TUSD", "15m", "mark"), # Uses mark candles (only bots with `trading_mode=futures`)
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
***
|
***
|
||||||
|
@ -232,7 +232,7 @@ graph = generate_candlestick_graph(pair=pair,
|
|||||||
# Show graph inline
|
# Show graph inline
|
||||||
# graph.show()
|
# graph.show()
|
||||||
|
|
||||||
# Render graph in a seperate window
|
# Render graph in a separate window
|
||||||
graph.show(renderer="browser")
|
graph.show(renderer="browser")
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -722,6 +722,7 @@ usage: freqtrade backtesting-analysis [-h] [-v] [--logfile FILE] [-V]
|
|||||||
[--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]]
|
[--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]]
|
||||||
[--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]]
|
[--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]]
|
||||||
[--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]]
|
[--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]]
|
||||||
|
[--timerange YYYYMMDD-[YYYYMMDD]]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
@ -744,6 +745,10 @@ optional arguments:
|
|||||||
--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]
|
--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]
|
||||||
Comma separated list of indicators to analyse. e.g.
|
Comma separated list of indicators to analyse. e.g.
|
||||||
'close,rsi,bb_lowerband,profit_abs'
|
'close,rsi,bb_lowerband,profit_abs'
|
||||||
|
--timerange YYYYMMDD-[YYYYMMDD]
|
||||||
|
Timerange to filter trades for analysis,
|
||||||
|
start inclusive, end exclusive. e.g.
|
||||||
|
20220101-20220201
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = '2022.11.dev'
|
__version__ = '2022.12.dev'
|
||||||
|
|
||||||
if 'dev' in __version__:
|
if 'dev' in __version__:
|
||||||
try:
|
try:
|
||||||
|
@ -60,10 +60,4 @@ def start_analysis_entries_exits(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
logger.info('Starting freqtrade in analysis mode')
|
logger.info('Starting freqtrade in analysis mode')
|
||||||
|
|
||||||
process_entry_exit_reasons(config['exportfilename'],
|
process_entry_exit_reasons(config)
|
||||||
config['exchange']['pair_whitelist'],
|
|
||||||
config['analysis_groups'],
|
|
||||||
config['enter_reason_list'],
|
|
||||||
config['exit_reason_list'],
|
|
||||||
config['indicator_list']
|
|
||||||
)
|
|
||||||
|
@ -106,7 +106,7 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
|
|||||||
"disableparamexport", "backtest_breakdown"]
|
"disableparamexport", "backtest_breakdown"]
|
||||||
|
|
||||||
ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list",
|
ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list",
|
||||||
"exit_reason_list", "indicator_list"]
|
"exit_reason_list", "indicator_list", "timerange"]
|
||||||
|
|
||||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||||
"list-markets", "list-pairs", "list-strategies", "list-freqaimodels",
|
"list-markets", "list-pairs", "list-strategies", "list-freqaimodels",
|
||||||
|
@ -462,6 +462,9 @@ class Configuration:
|
|||||||
self._args_to_config(config, argname='indicator_list',
|
self._args_to_config(config, argname='indicator_list',
|
||||||
logstring='Analysis indicator list: {}')
|
logstring='Analysis indicator list: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='timerange',
|
||||||
|
logstring='Filter trades by timerange: {}')
|
||||||
|
|
||||||
def _process_runmode(self, config: Config) -> None:
|
def _process_runmode(self, config: Config) -> None:
|
||||||
|
|
||||||
self._args_to_config(config, argname='dry_run',
|
self._args_to_config(config, argname='dry_run',
|
||||||
|
@ -3,11 +3,12 @@ This module contains the argument manager class
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
|
|
||||||
|
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
|
|
||||||
@ -29,6 +30,52 @@ class TimeRange:
|
|||||||
self.startts: int = startts
|
self.startts: int = startts
|
||||||
self.stopts: int = stopts
|
self.stopts: int = stopts
|
||||||
|
|
||||||
|
@property
|
||||||
|
def startdt(self) -> Optional[datetime]:
|
||||||
|
if self.startts:
|
||||||
|
return datetime.fromtimestamp(self.startts, tz=timezone.utc)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stopdt(self) -> Optional[datetime]:
|
||||||
|
if self.stopts:
|
||||||
|
return datetime.fromtimestamp(self.stopts, tz=timezone.utc)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timerange_str(self) -> str:
|
||||||
|
"""
|
||||||
|
Returns a string representation of the timerange as used by parse_timerange.
|
||||||
|
Follows the format yyyymmdd-yyyymmdd - leaving out the parts that are not set.
|
||||||
|
"""
|
||||||
|
start = ''
|
||||||
|
stop = ''
|
||||||
|
if startdt := self.startdt:
|
||||||
|
start = startdt.strftime('%Y%m%d')
|
||||||
|
if stopdt := self.stopdt:
|
||||||
|
stop = stopdt.strftime('%Y%m%d')
|
||||||
|
return f"{start}-{stop}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def start_fmt(self) -> str:
|
||||||
|
"""
|
||||||
|
Returns a string representation of the start date
|
||||||
|
"""
|
||||||
|
val = 'unbounded'
|
||||||
|
if (startdt := self.startdt) is not None:
|
||||||
|
val = startdt.strftime(DATETIME_PRINT_FORMAT)
|
||||||
|
return val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stop_fmt(self) -> str:
|
||||||
|
"""
|
||||||
|
Returns a string representation of the stop date
|
||||||
|
"""
|
||||||
|
val = 'unbounded'
|
||||||
|
if (stopdt := self.stopdt) is not None:
|
||||||
|
val = stopdt.strftime(DATETIME_PRINT_FORMAT)
|
||||||
|
return val
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
"""Override the default Equals behavior"""
|
"""Override the default Equals behavior"""
|
||||||
return (self.starttype == other.starttype and self.stoptype == other.stoptype
|
return (self.starttype == other.starttype and self.stoptype == other.stoptype
|
||||||
|
@ -512,6 +512,7 @@ CONF_SCHEMA = {
|
|||||||
'minimum': 0,
|
'minimum': 0,
|
||||||
'maximum': 65535
|
'maximum': 65535
|
||||||
},
|
},
|
||||||
|
'secure': {'type': 'boolean', 'default': False},
|
||||||
'ws_token': {'type': 'string'},
|
'ws_token': {'type': 'string'},
|
||||||
},
|
},
|
||||||
'required': ['name', 'host', 'ws_token']
|
'required': ['name', 'host', 'ws_token']
|
||||||
@ -577,9 +578,27 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"model_training_parameters": {
|
"model_training_parameters": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"rl_config": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"n_estimators": {"type": "integer", "default": 1000}
|
"train_cycles": {"type": "integer"},
|
||||||
|
"max_trade_duration_candles": {"type": "integer"},
|
||||||
|
"add_state_info": {"type": "boolean", "default": False},
|
||||||
|
"max_training_drawdown_pct": {"type": "number", "default": 0.02},
|
||||||
|
"cpu_count": {"type": "integer", "default": 1},
|
||||||
|
"model_type": {"type": "string", "default": "PPO"},
|
||||||
|
"policy_type": {"type": "string", "default": "MlpPolicy"},
|
||||||
|
"net_arch": {"type": "array", "default": [128, 128]},
|
||||||
|
"randomize_startinng_position": {"type": "boolean", "default": False},
|
||||||
|
"model_reward_parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"rr": {"type": "number", "default": 1},
|
||||||
|
"profit_aim": {"type": "number", "default": 0.025}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -3,7 +3,6 @@ Functions to convert data from one format to another
|
|||||||
"""
|
"""
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
@ -138,11 +137,9 @@ def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date',
|
|||||||
df = df.iloc[startup_candles:, :]
|
df = df.iloc[startup_candles:, :]
|
||||||
else:
|
else:
|
||||||
if timerange.starttype == 'date':
|
if timerange.starttype == 'date':
|
||||||
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
|
df = df.loc[df[df_date_col] >= timerange.startdt, :]
|
||||||
df = df.loc[df[df_date_col] >= start, :]
|
|
||||||
if timerange.stoptype == 'date':
|
if timerange.stoptype == 'date':
|
||||||
stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
|
df = df.loc[df[df_date_col] <= timerange.stopdt, :]
|
||||||
df = df.loc[df[df_date_col] <= stop, :]
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
import joblib
|
import joblib
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
|
from freqtrade.configuration import TimeRange
|
||||||
|
from freqtrade.constants import Config
|
||||||
from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data,
|
from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data,
|
||||||
load_backtest_stats)
|
load_backtest_stats)
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
@ -152,37 +153,55 @@ def _do_group_table_output(bigdf, glist):
|
|||||||
logger.warning("Invalid group mask specified.")
|
logger.warning("Invalid group mask specified.")
|
||||||
|
|
||||||
|
|
||||||
def _print_results(analysed_trades, stratname, analysis_groups,
|
def _select_rows_within_dates(df, timerange=None, df_date_col: str = 'date'):
|
||||||
enter_reason_list, exit_reason_list,
|
if timerange:
|
||||||
indicator_list, columns=None):
|
if timerange.starttype == 'date':
|
||||||
if columns is None:
|
df = df.loc[(df[df_date_col] >= timerange.startdt)]
|
||||||
columns = ['pair', 'open_date', 'close_date', 'profit_abs', 'enter_reason', 'exit_reason']
|
if timerange.stoptype == 'date':
|
||||||
|
df = df.loc[(df[df_date_col] < timerange.stopdt)]
|
||||||
|
return df
|
||||||
|
|
||||||
bigdf = pd.DataFrame()
|
|
||||||
for pair, trades in analysed_trades[stratname].items():
|
|
||||||
bigdf = pd.concat([bigdf, trades], ignore_index=True)
|
|
||||||
|
|
||||||
if bigdf.shape[0] > 0 and ('enter_reason' in bigdf.columns):
|
|
||||||
if analysis_groups:
|
|
||||||
_do_group_table_output(bigdf, analysis_groups)
|
|
||||||
|
|
||||||
|
def _select_rows_by_tags(df, enter_reason_list, exit_reason_list):
|
||||||
if enter_reason_list and "all" not in enter_reason_list:
|
if enter_reason_list and "all" not in enter_reason_list:
|
||||||
bigdf = bigdf.loc[(bigdf['enter_reason'].isin(enter_reason_list))]
|
df = df.loc[(df['enter_reason'].isin(enter_reason_list))]
|
||||||
|
|
||||||
if exit_reason_list and "all" not in exit_reason_list:
|
if exit_reason_list and "all" not in exit_reason_list:
|
||||||
bigdf = bigdf.loc[(bigdf['exit_reason'].isin(exit_reason_list))]
|
df = df.loc[(df['exit_reason'].isin(exit_reason_list))]
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_results(analysed_trades, stratname,
|
||||||
|
enter_reason_list, exit_reason_list,
|
||||||
|
timerange=None):
|
||||||
|
res_df = pd.DataFrame()
|
||||||
|
for pair, trades in analysed_trades[stratname].items():
|
||||||
|
res_df = pd.concat([res_df, trades], ignore_index=True)
|
||||||
|
|
||||||
|
res_df = _select_rows_within_dates(res_df, timerange)
|
||||||
|
|
||||||
|
if res_df is not None and res_df.shape[0] > 0 and ('enter_reason' in res_df.columns):
|
||||||
|
res_df = _select_rows_by_tags(res_df, enter_reason_list, exit_reason_list)
|
||||||
|
|
||||||
|
return res_df
|
||||||
|
|
||||||
|
|
||||||
|
def print_results(res_df, analysis_groups, indicator_list):
|
||||||
|
if res_df.shape[0] > 0:
|
||||||
|
if analysis_groups:
|
||||||
|
_do_group_table_output(res_df, analysis_groups)
|
||||||
|
|
||||||
if "all" in indicator_list:
|
if "all" in indicator_list:
|
||||||
print(bigdf)
|
print(res_df)
|
||||||
elif indicator_list is not None:
|
elif indicator_list is not None:
|
||||||
available_inds = []
|
available_inds = []
|
||||||
for ind in indicator_list:
|
for ind in indicator_list:
|
||||||
if ind in bigdf:
|
if ind in res_df:
|
||||||
available_inds.append(ind)
|
available_inds.append(ind)
|
||||||
ilist = ["pair", "enter_reason", "exit_reason"] + available_inds
|
ilist = ["pair", "enter_reason", "exit_reason"] + available_inds
|
||||||
_print_table(bigdf[ilist], sortcols=['exit_reason'], show_index=False)
|
_print_table(res_df[ilist], sortcols=['exit_reason'], show_index=False)
|
||||||
else:
|
else:
|
||||||
print("\\_ No trades to show")
|
print("\\No trades to show")
|
||||||
|
|
||||||
|
|
||||||
def _print_table(df, sortcols=None, show_index=False):
|
def _print_table(df, sortcols=None, show_index=False):
|
||||||
@ -201,26 +220,33 @@ def _print_table(df, sortcols=None, show_index=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def process_entry_exit_reasons(backtest_dir: Path,
|
def process_entry_exit_reasons(config: Config):
|
||||||
pairlist: List[str],
|
|
||||||
analysis_groups: Optional[List[str]] = ["0", "1", "2"],
|
|
||||||
enter_reason_list: Optional[List[str]] = ["all"],
|
|
||||||
exit_reason_list: Optional[List[str]] = ["all"],
|
|
||||||
indicator_list: Optional[List[str]] = []):
|
|
||||||
try:
|
try:
|
||||||
backtest_stats = load_backtest_stats(backtest_dir)
|
analysis_groups = config.get('analysis_groups', [])
|
||||||
|
enter_reason_list = config.get('enter_reason_list', ["all"])
|
||||||
|
exit_reason_list = config.get('exit_reason_list', ["all"])
|
||||||
|
indicator_list = config.get('indicator_list', [])
|
||||||
|
|
||||||
|
timerange = TimeRange.parse_timerange(None if config.get(
|
||||||
|
'timerange') is None else str(config.get('timerange')))
|
||||||
|
|
||||||
|
backtest_stats = load_backtest_stats(config['exportfilename'])
|
||||||
|
|
||||||
for strategy_name, results in backtest_stats['strategy'].items():
|
for strategy_name, results in backtest_stats['strategy'].items():
|
||||||
trades = load_backtest_data(backtest_dir, strategy_name)
|
trades = load_backtest_data(config['exportfilename'], strategy_name)
|
||||||
|
|
||||||
if not trades.empty:
|
if not trades.empty:
|
||||||
signal_candles = _load_signal_candles(backtest_dir)
|
signal_candles = _load_signal_candles(config['exportfilename'])
|
||||||
analysed_trades_dict = _process_candles_and_indicators(pairlist, strategy_name,
|
analysed_trades_dict = _process_candles_and_indicators(
|
||||||
|
config['exchange']['pair_whitelist'], strategy_name,
|
||||||
trades, signal_candles)
|
trades, signal_candles)
|
||||||
_print_results(analysed_trades_dict,
|
|
||||||
strategy_name,
|
res_df = prepare_results(analysed_trades_dict, strategy_name,
|
||||||
|
enter_reason_list, exit_reason_list,
|
||||||
|
timerange=timerange)
|
||||||
|
|
||||||
|
print_results(res_df,
|
||||||
analysis_groups,
|
analysis_groups,
|
||||||
enter_reason_list,
|
|
||||||
exit_reason_list,
|
|
||||||
indicator_list)
|
indicator_list)
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
@ -160,9 +160,9 @@ def _load_cached_data_for_updating(
|
|||||||
end = None
|
end = None
|
||||||
if timerange:
|
if timerange:
|
||||||
if timerange.starttype == 'date':
|
if timerange.starttype == 'date':
|
||||||
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
|
start = timerange.startdt
|
||||||
if timerange.stoptype == 'date':
|
if timerange.stoptype == 'date':
|
||||||
end = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
|
end = timerange.stopdt
|
||||||
|
|
||||||
# Intentionally don't pass timerange in - since we need to load the full dataset.
|
# Intentionally don't pass timerange in - since we need to load the full dataset.
|
||||||
data = data_handler.ohlcv_load(pair, timeframe=timeframe,
|
data = data_handler.ohlcv_load(pair, timeframe=timeframe,
|
||||||
|
@ -366,13 +366,11 @@ class IDataHandler(ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if timerange.starttype == 'date':
|
if timerange.starttype == 'date':
|
||||||
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
|
if pairdata.iloc[0]['date'] > timerange.startdt:
|
||||||
if pairdata.iloc[0]['date'] > start:
|
|
||||||
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
|
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
|
||||||
f"data starts at {pairdata.iloc[0]['date']:%Y-%m-%d %H:%M:%S}")
|
f"data starts at {pairdata.iloc[0]['date']:%Y-%m-%d %H:%M:%S}")
|
||||||
if timerange.stoptype == 'date':
|
if timerange.stoptype == 'date':
|
||||||
stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
|
if pairdata.iloc[-1]['date'] < timerange.stopdt:
|
||||||
if pairdata.iloc[-1]['date'] < stop:
|
|
||||||
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
|
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
|
||||||
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}")
|
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}")
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -20,7 +20,7 @@ class Bybit(Exchange):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"ohlcv_candle_limit": 200,
|
"ohlcv_candle_limit": 1000,
|
||||||
"ccxt_futures_name": "linear",
|
"ccxt_futures_name": "linear",
|
||||||
"ohlcv_has_history": False,
|
"ohlcv_has_history": False,
|
||||||
}
|
}
|
||||||
|
@ -218,3 +218,19 @@ class Kraken(Exchange):
|
|||||||
fees = sum(df['open_fund'] * df['open_mark'] * amount * time_in_ratio)
|
fees = sum(df['open_fund'] * df['open_mark'] * amount * time_in_ratio)
|
||||||
|
|
||||||
return fees if is_short else -fees
|
return fees if is_short else -fees
|
||||||
|
|
||||||
|
def _trades_contracts_to_amount(self, trades: List) -> List:
|
||||||
|
"""
|
||||||
|
Fix "last" id issue for kraken data downloads
|
||||||
|
This whole override can probably be removed once the following
|
||||||
|
issue is closed in ccxt: https://github.com/ccxt/ccxt/issues/15827
|
||||||
|
"""
|
||||||
|
super()._trades_contracts_to_amount(trades)
|
||||||
|
if (
|
||||||
|
len(trades) > 0
|
||||||
|
and isinstance(trades[-1].get('info'), list)
|
||||||
|
and len(trades[-1].get('info', [])) > 7
|
||||||
|
):
|
||||||
|
|
||||||
|
trades[-1]['id'] = trades[-1].get('info', [])[-1]
|
||||||
|
return trades
|
||||||
|
135
freqtrade/freqai/RL/Base4ActionRLEnv.py
Normal file
135
freqtrade/freqai/RL/Base4ActionRLEnv.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import logging
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from gym import spaces
|
||||||
|
|
||||||
|
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Actions(Enum):
|
||||||
|
Neutral = 0
|
||||||
|
Exit = 1
|
||||||
|
Long_enter = 2
|
||||||
|
Short_enter = 3
|
||||||
|
|
||||||
|
|
||||||
|
class Base4ActionRLEnv(BaseEnvironment):
|
||||||
|
"""
|
||||||
|
Base class for a 4 action environment
|
||||||
|
"""
|
||||||
|
|
||||||
|
def set_action_space(self):
|
||||||
|
self.action_space = spaces.Discrete(len(Actions))
|
||||||
|
|
||||||
|
def step(self, action: int):
|
||||||
|
"""
|
||||||
|
Logic for a single step (incrementing one candle in time)
|
||||||
|
by the agent
|
||||||
|
:param: action: int = the action type that the agent plans
|
||||||
|
to take for the current step.
|
||||||
|
:returns:
|
||||||
|
observation = current state of environment
|
||||||
|
step_reward = the reward from `calculate_reward()`
|
||||||
|
_done = if the agent "died" or if the candles finished
|
||||||
|
info = dict passed back to openai gym lib
|
||||||
|
"""
|
||||||
|
self._done = False
|
||||||
|
self._current_tick += 1
|
||||||
|
|
||||||
|
if self._current_tick == self._end_tick:
|
||||||
|
self._done = True
|
||||||
|
|
||||||
|
self._update_unrealized_total_profit()
|
||||||
|
|
||||||
|
step_reward = self.calculate_reward(action)
|
||||||
|
self.total_reward += step_reward
|
||||||
|
|
||||||
|
trade_type = None
|
||||||
|
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:
|
||||||
|
self._position = Positions.Neutral
|
||||||
|
trade_type = "neutral"
|
||||||
|
self._last_trade_tick = None
|
||||||
|
elif action == Actions.Long_enter.value:
|
||||||
|
self._position = Positions.Long
|
||||||
|
trade_type = "long"
|
||||||
|
self._last_trade_tick = self._current_tick
|
||||||
|
elif action == Actions.Short_enter.value:
|
||||||
|
self._position = Positions.Short
|
||||||
|
trade_type = "short"
|
||||||
|
self._last_trade_tick = self._current_tick
|
||||||
|
elif action == Actions.Exit.value:
|
||||||
|
self._update_total_profit()
|
||||||
|
self._position = Positions.Neutral
|
||||||
|
trade_type = "neutral"
|
||||||
|
self._last_trade_tick = None
|
||||||
|
else:
|
||||||
|
print("case not defined")
|
||||||
|
|
||||||
|
if trade_type is not None:
|
||||||
|
self.trade_history.append(
|
||||||
|
{'price': self.current_price(), 'index': self._current_tick,
|
||||||
|
'type': trade_type})
|
||||||
|
|
||||||
|
if self._total_profit < 1 - self.rl_config.get('max_training_drawdown_pct', 0.8):
|
||||||
|
self._done = True
|
||||||
|
|
||||||
|
self._position_history.append(self._position)
|
||||||
|
|
||||||
|
info = dict(
|
||||||
|
tick=self._current_tick,
|
||||||
|
total_reward=self.total_reward,
|
||||||
|
total_profit=self._total_profit,
|
||||||
|
position=self._position.value
|
||||||
|
)
|
||||||
|
|
||||||
|
observation = self._get_observation()
|
||||||
|
|
||||||
|
self._update_history(info)
|
||||||
|
|
||||||
|
return observation, step_reward, self._done, info
|
||||||
|
|
||||||
|
def is_tradesignal(self, action: int) -> bool:
|
||||||
|
"""
|
||||||
|
Determine if the signal is a trade signal
|
||||||
|
e.g.: agent wants a Actions.Long_exit while it is in a Positions.short
|
||||||
|
"""
|
||||||
|
return not ((action == Actions.Neutral.value and self._position == Positions.Neutral) or
|
||||||
|
(action == Actions.Neutral.value and self._position == Positions.Short) or
|
||||||
|
(action == Actions.Neutral.value and self._position == Positions.Long) or
|
||||||
|
(action == Actions.Short_enter.value and self._position == Positions.Short) or
|
||||||
|
(action == Actions.Short_enter.value and self._position == Positions.Long) or
|
||||||
|
(action == Actions.Exit.value and self._position == Positions.Neutral) or
|
||||||
|
(action == Actions.Long_enter.value and self._position == Positions.Long) or
|
||||||
|
(action == Actions.Long_enter.value and self._position == Positions.Short))
|
||||||
|
|
||||||
|
def _is_valid(self, action: int) -> bool:
|
||||||
|
"""
|
||||||
|
Determine if the signal is valid.
|
||||||
|
e.g.: agent wants a Actions.Long_exit while it is in a Positions.short
|
||||||
|
"""
|
||||||
|
# Agent should only try to exit if it is in position
|
||||||
|
if action == Actions.Exit.value:
|
||||||
|
if self._position not in (Positions.Short, Positions.Long):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Agent should only try to enter if it is not in position
|
||||||
|
if action in (Actions.Short_enter.value, Actions.Long_enter.value):
|
||||||
|
if self._position != Positions.Neutral:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
145
freqtrade/freqai/RL/Base5ActionRLEnv.py
Normal file
145
freqtrade/freqai/RL/Base5ActionRLEnv.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import logging
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from gym import spaces
|
||||||
|
|
||||||
|
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Actions(Enum):
|
||||||
|
Neutral = 0
|
||||||
|
Long_enter = 1
|
||||||
|
Long_exit = 2
|
||||||
|
Short_enter = 3
|
||||||
|
Short_exit = 4
|
||||||
|
|
||||||
|
|
||||||
|
class Base5ActionRLEnv(BaseEnvironment):
|
||||||
|
"""
|
||||||
|
Base class for a 5 action environment
|
||||||
|
"""
|
||||||
|
|
||||||
|
def set_action_space(self):
|
||||||
|
self.action_space = spaces.Discrete(len(Actions))
|
||||||
|
|
||||||
|
def step(self, action: int):
|
||||||
|
"""
|
||||||
|
Logic for a single step (incrementing one candle in time)
|
||||||
|
by the agent
|
||||||
|
:param: action: int = the action type that the agent plans
|
||||||
|
to take for the current step.
|
||||||
|
:returns:
|
||||||
|
observation = current state of environment
|
||||||
|
step_reward = the reward from `calculate_reward()`
|
||||||
|
_done = if the agent "died" or if the candles finished
|
||||||
|
info = dict passed back to openai gym lib
|
||||||
|
"""
|
||||||
|
self._done = False
|
||||||
|
self._current_tick += 1
|
||||||
|
|
||||||
|
if self._current_tick == self._end_tick:
|
||||||
|
self._done = True
|
||||||
|
|
||||||
|
self._update_unrealized_total_profit()
|
||||||
|
step_reward = self.calculate_reward(action)
|
||||||
|
self.total_reward += step_reward
|
||||||
|
|
||||||
|
trade_type = None
|
||||||
|
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:
|
||||||
|
self._position = Positions.Neutral
|
||||||
|
trade_type = "neutral"
|
||||||
|
self._last_trade_tick = None
|
||||||
|
elif action == Actions.Long_enter.value:
|
||||||
|
self._position = Positions.Long
|
||||||
|
trade_type = "long"
|
||||||
|
self._last_trade_tick = self._current_tick
|
||||||
|
elif action == Actions.Short_enter.value:
|
||||||
|
self._position = Positions.Short
|
||||||
|
trade_type = "short"
|
||||||
|
self._last_trade_tick = self._current_tick
|
||||||
|
elif action == Actions.Long_exit.value:
|
||||||
|
self._update_total_profit()
|
||||||
|
self._position = Positions.Neutral
|
||||||
|
trade_type = "neutral"
|
||||||
|
self._last_trade_tick = None
|
||||||
|
elif action == Actions.Short_exit.value:
|
||||||
|
self._update_total_profit()
|
||||||
|
self._position = Positions.Neutral
|
||||||
|
trade_type = "neutral"
|
||||||
|
self._last_trade_tick = None
|
||||||
|
else:
|
||||||
|
print("case not defined")
|
||||||
|
|
||||||
|
if trade_type is not None:
|
||||||
|
self.trade_history.append(
|
||||||
|
{'price': self.current_price(), 'index': self._current_tick,
|
||||||
|
'type': trade_type})
|
||||||
|
|
||||||
|
if (self._total_profit < self.max_drawdown or
|
||||||
|
self._total_unrealized_profit < self.max_drawdown):
|
||||||
|
self._done = True
|
||||||
|
|
||||||
|
self._position_history.append(self._position)
|
||||||
|
|
||||||
|
info = dict(
|
||||||
|
tick=self._current_tick,
|
||||||
|
total_reward=self.total_reward,
|
||||||
|
total_profit=self._total_profit,
|
||||||
|
position=self._position.value
|
||||||
|
)
|
||||||
|
|
||||||
|
observation = self._get_observation()
|
||||||
|
|
||||||
|
self._update_history(info)
|
||||||
|
|
||||||
|
return observation, step_reward, self._done, info
|
||||||
|
|
||||||
|
def is_tradesignal(self, action: int) -> bool:
|
||||||
|
"""
|
||||||
|
Determine if the signal is a trade signal
|
||||||
|
e.g.: agent wants a Actions.Long_exit while it is in a Positions.short
|
||||||
|
"""
|
||||||
|
return not ((action == Actions.Neutral.value and self._position == Positions.Neutral) or
|
||||||
|
(action == Actions.Neutral.value and self._position == Positions.Short) or
|
||||||
|
(action == Actions.Neutral.value and self._position == Positions.Long) or
|
||||||
|
(action == Actions.Short_enter.value and self._position == Positions.Short) or
|
||||||
|
(action == Actions.Short_enter.value and self._position == Positions.Long) or
|
||||||
|
(action == Actions.Short_exit.value and self._position == Positions.Long) or
|
||||||
|
(action == Actions.Short_exit.value and self._position == Positions.Neutral) or
|
||||||
|
(action == Actions.Long_enter.value and self._position == Positions.Long) or
|
||||||
|
(action == Actions.Long_enter.value and self._position == Positions.Short) or
|
||||||
|
(action == Actions.Long_exit.value and self._position == Positions.Short) or
|
||||||
|
(action == Actions.Long_exit.value and self._position == Positions.Neutral))
|
||||||
|
|
||||||
|
def _is_valid(self, action: int) -> bool:
|
||||||
|
# trade signal
|
||||||
|
"""
|
||||||
|
Determine if the signal is valid.
|
||||||
|
e.g.: agent wants a Actions.Long_exit while it is in a Positions.short
|
||||||
|
"""
|
||||||
|
# Agent should only try to exit if it is in position
|
||||||
|
if action in (Actions.Short_exit.value, Actions.Long_exit.value):
|
||||||
|
if self._position not in (Positions.Short, Positions.Long):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Agent should only try to enter if it is not in position
|
||||||
|
if action in (Actions.Short_enter.value, Actions.Long_enter.value):
|
||||||
|
if self._position != Positions.Neutral:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
307
freqtrade/freqai/RL/BaseEnvironment.py
Normal file
307
freqtrade/freqai/RL/BaseEnvironment.py
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
import logging
|
||||||
|
import random
|
||||||
|
from abc import abstractmethod
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import gym
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from gym import spaces
|
||||||
|
from gym.utils import seeding
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Positions(Enum):
|
||||||
|
Short = 0
|
||||||
|
Long = 1
|
||||||
|
Neutral = 0.5
|
||||||
|
|
||||||
|
def opposite(self):
|
||||||
|
return Positions.Short if self == Positions.Long else Positions.Long
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEnvironment(gym.Env):
|
||||||
|
"""
|
||||||
|
Base class for environments. This class is agnostic to action count.
|
||||||
|
Inherited classes customize this to include varying action counts/types,
|
||||||
|
See RL/Base5ActionRLEnv.py and RL/Base4ActionRLEnv.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, df: DataFrame = DataFrame(), prices: DataFrame = DataFrame(),
|
||||||
|
reward_kwargs: dict = {}, window_size=10, starting_point=True,
|
||||||
|
id: str = 'baseenv-1', seed: int = 1, config: dict = {},
|
||||||
|
dp: Optional[DataProvider] = None):
|
||||||
|
"""
|
||||||
|
Initializes the training/eval environment.
|
||||||
|
:param df: dataframe of features
|
||||||
|
:param prices: dataframe of prices to be used in the training environment
|
||||||
|
:param window_size: size of window (temporal) to pass to the agent
|
||||||
|
:param reward_kwargs: extra config settings assigned by user in `rl_config`
|
||||||
|
:param starting_point: start at edge of window or not
|
||||||
|
:param id: string id of the environment (used in backend for multiprocessed env)
|
||||||
|
:param seed: Sets the seed of the environment higher in the gym.Env object
|
||||||
|
:param config: Typical user configuration file
|
||||||
|
:param dp: dataprovider from freqtrade
|
||||||
|
"""
|
||||||
|
self.config = config
|
||||||
|
self.rl_config = config['freqai']['rl_config']
|
||||||
|
self.add_state_info = self.rl_config.get('add_state_info', False)
|
||||||
|
self.id = id
|
||||||
|
self.seed(seed)
|
||||||
|
self.reset_env(df, prices, window_size, reward_kwargs, starting_point)
|
||||||
|
self.max_drawdown = 1 - self.rl_config.get('max_training_drawdown_pct', 0.8)
|
||||||
|
self.compound_trades = config['stake_amount'] == 'unlimited'
|
||||||
|
if self.config.get('fee', None) is not None:
|
||||||
|
self.fee = self.config['fee']
|
||||||
|
elif dp is not None:
|
||||||
|
self.fee = dp._exchange.get_fee(symbol=dp.current_whitelist()[0]) # type: ignore
|
||||||
|
else:
|
||||||
|
self.fee = 0.0015
|
||||||
|
|
||||||
|
def reset_env(self, df: DataFrame, prices: DataFrame, window_size: int,
|
||||||
|
reward_kwargs: dict, starting_point=True):
|
||||||
|
"""
|
||||||
|
Resets the environment when the agent fails (in our case, if the drawdown
|
||||||
|
exceeds the user set max_training_drawdown_pct)
|
||||||
|
:param df: dataframe of features
|
||||||
|
:param prices: dataframe of prices to be used in the training environment
|
||||||
|
:param window_size: size of window (temporal) to pass to the agent
|
||||||
|
:param reward_kwargs: extra config settings assigned by user in `rl_config`
|
||||||
|
:param starting_point: start at edge of window or not
|
||||||
|
"""
|
||||||
|
self.df = df
|
||||||
|
self.signal_features = self.df
|
||||||
|
self.prices = prices
|
||||||
|
self.window_size = window_size
|
||||||
|
self.starting_point = starting_point
|
||||||
|
self.rr = reward_kwargs["rr"]
|
||||||
|
self.profit_aim = reward_kwargs["profit_aim"]
|
||||||
|
|
||||||
|
# # spaces
|
||||||
|
if self.add_state_info:
|
||||||
|
self.total_features = self.signal_features.shape[1] + 3
|
||||||
|
else:
|
||||||
|
self.total_features = self.signal_features.shape[1]
|
||||||
|
self.shape = (window_size, self.total_features)
|
||||||
|
self.set_action_space()
|
||||||
|
self.observation_space = spaces.Box(
|
||||||
|
low=-1, high=1, shape=self.shape, dtype=np.float32)
|
||||||
|
|
||||||
|
# episode
|
||||||
|
self._start_tick: int = self.window_size
|
||||||
|
self._end_tick: int = len(self.prices) - 1
|
||||||
|
self._done: bool = False
|
||||||
|
self._current_tick: int = self._start_tick
|
||||||
|
self._last_trade_tick: Optional[int] = None
|
||||||
|
self._position = Positions.Neutral
|
||||||
|
self._position_history: list = [None]
|
||||||
|
self.total_reward: float = 0
|
||||||
|
self._total_profit: float = 1
|
||||||
|
self._total_unrealized_profit: float = 1
|
||||||
|
self.history: dict = {}
|
||||||
|
self.trade_history: list = []
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_action_space(self):
|
||||||
|
"""
|
||||||
|
Unique to the environment action count. Must be inherited.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def seed(self, seed: int = 1):
|
||||||
|
self.np_random, seed = seeding.np_random(seed)
|
||||||
|
return [seed]
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
|
||||||
|
self._done = False
|
||||||
|
|
||||||
|
if self.starting_point is True:
|
||||||
|
if self.rl_config.get('randomize_starting_position', False):
|
||||||
|
length_of_data = int(self._end_tick / 4)
|
||||||
|
start_tick = random.randint(self.window_size + 1, length_of_data)
|
||||||
|
self._start_tick = start_tick
|
||||||
|
self._position_history = (self._start_tick * [None]) + [self._position]
|
||||||
|
else:
|
||||||
|
self._position_history = (self.window_size * [None]) + [self._position]
|
||||||
|
|
||||||
|
self._current_tick = self._start_tick
|
||||||
|
self._last_trade_tick = None
|
||||||
|
self._position = Positions.Neutral
|
||||||
|
|
||||||
|
self.total_reward = 0.
|
||||||
|
self._total_profit = 1. # unit
|
||||||
|
self.history = {}
|
||||||
|
self.trade_history = []
|
||||||
|
self.portfolio_log_returns = np.zeros(len(self.prices))
|
||||||
|
|
||||||
|
self._profits = [(self._start_tick, 1)]
|
||||||
|
self.close_trade_profit = []
|
||||||
|
self._total_unrealized_profit = 1
|
||||||
|
|
||||||
|
return self._get_observation()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def step(self, action: int):
|
||||||
|
"""
|
||||||
|
Step depeneds on action types, this must be inherited.
|
||||||
|
"""
|
||||||
|
return
|
||||||
|
|
||||||
|
def _get_observation(self):
|
||||||
|
"""
|
||||||
|
This may or may not be independent of action types, user can inherit
|
||||||
|
this in their custom "MyRLEnv"
|
||||||
|
"""
|
||||||
|
features_window = self.signal_features[(
|
||||||
|
self._current_tick - self.window_size):self._current_tick]
|
||||||
|
if self.add_state_info:
|
||||||
|
features_and_state = DataFrame(np.zeros((len(features_window), 3)),
|
||||||
|
columns=['current_profit_pct',
|
||||||
|
'position',
|
||||||
|
'trade_duration'],
|
||||||
|
index=features_window.index)
|
||||||
|
|
||||||
|
features_and_state['current_profit_pct'] = self.get_unrealized_profit()
|
||||||
|
features_and_state['position'] = self._position.value
|
||||||
|
features_and_state['trade_duration'] = self.get_trade_duration()
|
||||||
|
features_and_state = pd.concat([features_window, features_and_state], axis=1)
|
||||||
|
return features_and_state
|
||||||
|
else:
|
||||||
|
return features_window
|
||||||
|
|
||||||
|
def get_trade_duration(self):
|
||||||
|
"""
|
||||||
|
Get the trade duration if the agent is in a trade
|
||||||
|
"""
|
||||||
|
if self._last_trade_tick is None:
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
return self._current_tick - self._last_trade_tick
|
||||||
|
|
||||||
|
def get_unrealized_profit(self):
|
||||||
|
"""
|
||||||
|
Get the unrealized profit if the agent is in a trade
|
||||||
|
"""
|
||||||
|
if self._last_trade_tick is None:
|
||||||
|
return 0.
|
||||||
|
|
||||||
|
if self._position == Positions.Neutral:
|
||||||
|
return 0.
|
||||||
|
elif self._position == Positions.Short:
|
||||||
|
current_price = self.add_exit_fee(self.prices.iloc[self._current_tick].open)
|
||||||
|
last_trade_price = self.add_entry_fee(self.prices.iloc[self._last_trade_tick].open)
|
||||||
|
return (last_trade_price - current_price) / last_trade_price
|
||||||
|
elif self._position == Positions.Long:
|
||||||
|
current_price = self.add_entry_fee(self.prices.iloc[self._current_tick].open)
|
||||||
|
last_trade_price = self.add_exit_fee(self.prices.iloc[self._last_trade_tick].open)
|
||||||
|
return (current_price - last_trade_price) / last_trade_price
|
||||||
|
else:
|
||||||
|
return 0.
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def is_tradesignal(self, action: int) -> bool:
|
||||||
|
"""
|
||||||
|
Determine if the signal is a trade signal. This is
|
||||||
|
unique to the actions in the environment, and therefore must be
|
||||||
|
inherited.
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _is_valid(self, action: int) -> bool:
|
||||||
|
"""
|
||||||
|
Determine if the signal is valid.This is
|
||||||
|
unique to the actions in the environment, and therefore must be
|
||||||
|
inherited.
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add_entry_fee(self, price):
|
||||||
|
return price * (1 + self.fee)
|
||||||
|
|
||||||
|
def add_exit_fee(self, price):
|
||||||
|
return price / (1 + self.fee)
|
||||||
|
|
||||||
|
def _update_history(self, info):
|
||||||
|
if not self.history:
|
||||||
|
self.history = {key: [] for key in info.keys()}
|
||||||
|
|
||||||
|
for key, value in info.items():
|
||||||
|
self.history[key].append(value)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def calculate_reward(self, action: int) -> float:
|
||||||
|
"""
|
||||||
|
An example reward function. This is the one function that users will likely
|
||||||
|
wish to inject their own creativity into.
|
||||||
|
:param action: int = The action made by the agent for the current candle.
|
||||||
|
:return:
|
||||||
|
float = the reward to give to the agent for current step (used for optimization
|
||||||
|
of weights in NN)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _update_unrealized_total_profit(self):
|
||||||
|
"""
|
||||||
|
Update the unrealized total profit incase of episode end.
|
||||||
|
"""
|
||||||
|
if self._position in (Positions.Long, Positions.Short):
|
||||||
|
pnl = self.get_unrealized_profit()
|
||||||
|
if self.compound_trades:
|
||||||
|
# assumes unit stake and compounding
|
||||||
|
unrl_profit = self._total_profit * (1 + pnl)
|
||||||
|
else:
|
||||||
|
# assumes unit stake and no compounding
|
||||||
|
unrl_profit = self._total_profit + pnl
|
||||||
|
self._total_unrealized_profit = unrl_profit
|
||||||
|
|
||||||
|
def _update_total_profit(self):
|
||||||
|
pnl = self.get_unrealized_profit()
|
||||||
|
if self.compound_trades:
|
||||||
|
# assumes unit stake and compounding
|
||||||
|
self._total_profit = self._total_profit * (1 + pnl)
|
||||||
|
else:
|
||||||
|
# assumes unit stake and no compounding
|
||||||
|
self._total_profit += pnl
|
||||||
|
|
||||||
|
def current_price(self) -> float:
|
||||||
|
return self.prices.iloc[self._current_tick].open
|
||||||
|
|
||||||
|
# Keeping around incase we want to start building more complex environment
|
||||||
|
# templates in the future.
|
||||||
|
# def most_recent_return(self):
|
||||||
|
# """
|
||||||
|
# Calculate the tick to tick return if in a trade.
|
||||||
|
# Return is generated from rising prices in Long
|
||||||
|
# and falling prices in Short positions.
|
||||||
|
# The actions Sell/Buy or Hold during a Long position trigger the sell/buy-fee.
|
||||||
|
# """
|
||||||
|
# # Long positions
|
||||||
|
# if self._position == Positions.Long:
|
||||||
|
# current_price = self.prices.iloc[self._current_tick].open
|
||||||
|
# previous_price = self.prices.iloc[self._current_tick - 1].open
|
||||||
|
|
||||||
|
# if (self._position_history[self._current_tick - 1] == Positions.Short
|
||||||
|
# or self._position_history[self._current_tick - 1] == Positions.Neutral):
|
||||||
|
# previous_price = self.add_entry_fee(previous_price)
|
||||||
|
|
||||||
|
# return np.log(current_price) - np.log(previous_price)
|
||||||
|
|
||||||
|
# # Short positions
|
||||||
|
# if self._position == Positions.Short:
|
||||||
|
# current_price = self.prices.iloc[self._current_tick].open
|
||||||
|
# previous_price = self.prices.iloc[self._current_tick - 1].open
|
||||||
|
# if (self._position_history[self._current_tick - 1] == Positions.Long
|
||||||
|
# or self._position_history[self._current_tick - 1] == Positions.Neutral):
|
||||||
|
# previous_price = self.add_exit_fee(previous_price)
|
||||||
|
|
||||||
|
# return np.log(previous_price) - np.log(current_price)
|
||||||
|
|
||||||
|
# return 0
|
||||||
|
|
||||||
|
# def update_portfolio_log_returns(self, action):
|
||||||
|
# self.portfolio_log_returns[self._current_tick] = self.most_recent_return(action)
|
400
freqtrade/freqai/RL/BaseReinforcementLearningModel.py
Normal file
400
freqtrade/freqai/RL/BaseReinforcementLearningModel.py
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
from abc import abstractmethod
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable, Dict, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
|
import gym
|
||||||
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
|
import pandas as pd
|
||||||
|
import torch as th
|
||||||
|
import torch.multiprocessing
|
||||||
|
from pandas import DataFrame
|
||||||
|
from stable_baselines3.common.callbacks import EvalCallback
|
||||||
|
from stable_baselines3.common.monitor import Monitor
|
||||||
|
from stable_baselines3.common.utils import set_random_seed
|
||||||
|
from stable_baselines3.common.vec_env import SubprocVecEnv
|
||||||
|
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
|
from freqtrade.freqai.freqai_interface import IFreqaiModel
|
||||||
|
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv
|
||||||
|
from freqtrade.freqai.RL.BaseEnvironment import Positions
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
torch.multiprocessing.set_sharing_strategy('file_system')
|
||||||
|
|
||||||
|
SB3_MODELS = ['PPO', 'A2C', 'DQN']
|
||||||
|
SB3_CONTRIB_MODELS = ['TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO']
|
||||||
|
|
||||||
|
|
||||||
|
class BaseReinforcementLearningModel(IFreqaiModel):
|
||||||
|
"""
|
||||||
|
User created Reinforcement Learning Model prediction class
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
super().__init__(config=kwargs['config'])
|
||||||
|
self.max_threads = min(self.freqai_info['rl_config'].get(
|
||||||
|
'cpu_count', 1), max(int(self.max_system_threads / 2), 1))
|
||||||
|
th.set_num_threads(self.max_threads)
|
||||||
|
self.reward_params = self.freqai_info['rl_config']['model_reward_parameters']
|
||||||
|
self.train_env: Union[SubprocVecEnv, gym.Env] = None
|
||||||
|
self.eval_env: Union[SubprocVecEnv, gym.Env] = None
|
||||||
|
self.eval_callback: Optional[EvalCallback] = None
|
||||||
|
self.model_type = self.freqai_info['rl_config']['model_type']
|
||||||
|
self.rl_config = self.freqai_info['rl_config']
|
||||||
|
self.continual_learning = self.freqai_info.get('continual_learning', False)
|
||||||
|
if self.model_type in SB3_MODELS:
|
||||||
|
import_str = 'stable_baselines3'
|
||||||
|
elif self.model_type in SB3_CONTRIB_MODELS:
|
||||||
|
import_str = 'sb3_contrib'
|
||||||
|
else:
|
||||||
|
raise OperationalException(f'{self.model_type} not available in stable_baselines3 or '
|
||||||
|
f'sb3_contrib. please choose one of {SB3_MODELS} or '
|
||||||
|
f'{SB3_CONTRIB_MODELS}')
|
||||||
|
|
||||||
|
mod = importlib.import_module(import_str, self.model_type)
|
||||||
|
self.MODELCLASS = getattr(mod, self.model_type)
|
||||||
|
self.policy_type = self.freqai_info['rl_config']['policy_type']
|
||||||
|
self.unset_outlier_removal()
|
||||||
|
self.net_arch = self.rl_config.get('net_arch', [128, 128])
|
||||||
|
self.dd.model_type = "stable_baselines"
|
||||||
|
|
||||||
|
def unset_outlier_removal(self):
|
||||||
|
"""
|
||||||
|
If user has activated any function that may remove training points, this
|
||||||
|
function will set them to false and warn them
|
||||||
|
"""
|
||||||
|
if self.ft_params.get('use_SVM_to_remove_outliers', False):
|
||||||
|
self.ft_params.update({'use_SVM_to_remove_outliers': False})
|
||||||
|
logger.warning('User tried to use SVM with RL. Deactivating SVM.')
|
||||||
|
if self.ft_params.get('use_DBSCAN_to_remove_outliers', False):
|
||||||
|
self.ft_params.update({'use_DBSCAN_to_remove_outliers': False})
|
||||||
|
logger.warning('User tried to use DBSCAN with RL. Deactivating DBSCAN.')
|
||||||
|
if self.freqai_info['data_split_parameters'].get('shuffle', False):
|
||||||
|
self.freqai_info['data_split_parameters'].update({'shuffle': False})
|
||||||
|
logger.warning('User tried to shuffle training data. Setting shuffle to False')
|
||||||
|
|
||||||
|
def train(
|
||||||
|
self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Filter the training data and train a model to it. Train makes heavy use of the datakitchen
|
||||||
|
for storing, saving, loading, and analyzing the data.
|
||||||
|
:param unfiltered_df: Full dataframe for the current training period
|
||||||
|
:param metadata: pair metadata from strategy.
|
||||||
|
:returns:
|
||||||
|
:model: Trained model which can be used to inference (self.predict)
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info("--------------------Starting training " f"{pair} --------------------")
|
||||||
|
|
||||||
|
features_filtered, labels_filtered = dk.filter_features(
|
||||||
|
unfiltered_df,
|
||||||
|
dk.training_features_list,
|
||||||
|
dk.label_list,
|
||||||
|
training_filter=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
data_dictionary: Dict[str, Any] = dk.make_train_test_datasets(
|
||||||
|
features_filtered, labels_filtered)
|
||||||
|
dk.fit_labels() # FIXME useless for now, but just satiating append methods
|
||||||
|
|
||||||
|
# normalize all data based on train_dataset only
|
||||||
|
prices_train, prices_test = self.build_ohlc_price_dataframes(dk.data_dictionary, pair, dk)
|
||||||
|
data_dictionary = dk.normalize_data(data_dictionary)
|
||||||
|
|
||||||
|
# data cleaning/analysis
|
||||||
|
self.data_cleaning_train(dk)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f'Training model on {len(dk.data_dictionary["train_features"].columns)}'
|
||||||
|
f' features and {len(data_dictionary["train_features"])} data points'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.set_train_and_eval_environments(data_dictionary, prices_train, prices_test, dk)
|
||||||
|
|
||||||
|
model = self.fit(data_dictionary, dk)
|
||||||
|
|
||||||
|
logger.info(f"--------------------done training {pair}--------------------")
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
def set_train_and_eval_environments(self, data_dictionary: Dict[str, DataFrame],
|
||||||
|
prices_train: DataFrame, prices_test: DataFrame,
|
||||||
|
dk: FreqaiDataKitchen):
|
||||||
|
"""
|
||||||
|
User can override this if they are using a custom MyRLEnv
|
||||||
|
:param data_dictionary: dict = common data dictionary containing train and test
|
||||||
|
features/labels/weights.
|
||||||
|
:param prices_train/test: DataFrame = dataframe comprised of the prices to be used in the
|
||||||
|
environment during training or testing
|
||||||
|
:param dk: FreqaiDataKitchen = the datakitchen for the current pair
|
||||||
|
"""
|
||||||
|
train_df = data_dictionary["train_features"]
|
||||||
|
test_df = data_dictionary["test_features"]
|
||||||
|
|
||||||
|
self.train_env = self.MyRLEnv(df=train_df,
|
||||||
|
prices=prices_train,
|
||||||
|
window_size=self.CONV_WIDTH,
|
||||||
|
reward_kwargs=self.reward_params,
|
||||||
|
config=self.config,
|
||||||
|
dp=self.data_provider)
|
||||||
|
self.eval_env = Monitor(self.MyRLEnv(df=test_df,
|
||||||
|
prices=prices_test,
|
||||||
|
window_size=self.CONV_WIDTH,
|
||||||
|
reward_kwargs=self.reward_params,
|
||||||
|
config=self.config,
|
||||||
|
dp=self.data_provider))
|
||||||
|
self.eval_callback = EvalCallback(self.eval_env, deterministic=True,
|
||||||
|
render=False, eval_freq=len(train_df),
|
||||||
|
best_model_save_path=str(dk.data_path))
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen, **kwargs):
|
||||||
|
"""
|
||||||
|
Agent customizations and abstract Reinforcement Learning customizations
|
||||||
|
go in here. Abstract method, so this function must be overridden by
|
||||||
|
user class.
|
||||||
|
"""
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_state_info(self, pair: str) -> Tuple[float, float, int]:
|
||||||
|
"""
|
||||||
|
State info during dry/live (not backtesting) which is fed back
|
||||||
|
into the model.
|
||||||
|
:param pair: str = COIN/STAKE to get the environment information for
|
||||||
|
:return:
|
||||||
|
:market_side: float = representing short, long, or neutral for
|
||||||
|
pair
|
||||||
|
:current_profit: float = unrealized profit of the current trade
|
||||||
|
:trade_duration: int = the number of candles that the trade has
|
||||||
|
been open for
|
||||||
|
"""
|
||||||
|
open_trades = Trade.get_trades_proxy(is_open=True)
|
||||||
|
market_side = 0.5
|
||||||
|
current_profit: float = 0
|
||||||
|
trade_duration = 0
|
||||||
|
for trade in open_trades:
|
||||||
|
if trade.pair == pair:
|
||||||
|
if self.data_provider._exchange is None: # type: ignore
|
||||||
|
logger.error('No exchange available.')
|
||||||
|
return 0, 0, 0
|
||||||
|
else:
|
||||||
|
current_rate = self.data_provider._exchange.get_rate( # type: ignore
|
||||||
|
pair, refresh=False, side="exit", is_short=trade.is_short)
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc).timestamp()
|
||||||
|
trade_duration = int((now - trade.open_date_utc.timestamp()) / self.base_tf_seconds)
|
||||||
|
current_profit = trade.calc_profit_ratio(current_rate)
|
||||||
|
if trade.is_short:
|
||||||
|
market_side = 0
|
||||||
|
else:
|
||||||
|
market_side = 1
|
||||||
|
|
||||||
|
return market_side, current_profit, int(trade_duration)
|
||||||
|
|
||||||
|
def predict(
|
||||||
|
self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs
|
||||||
|
) -> Tuple[DataFrame, npt.NDArray[np.int_]]:
|
||||||
|
"""
|
||||||
|
Filter the prediction features data and predict with it.
|
||||||
|
:param unfiltered_dataframe: Full dataframe for the current backtest period.
|
||||||
|
:return:
|
||||||
|
:pred_df: dataframe containing the predictions
|
||||||
|
:do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove
|
||||||
|
data (NaNs) or felt uncertain about data (PCA and DI index)
|
||||||
|
"""
|
||||||
|
|
||||||
|
dk.find_features(unfiltered_df)
|
||||||
|
filtered_dataframe, _ = dk.filter_features(
|
||||||
|
unfiltered_df, dk.training_features_list, training_filter=False
|
||||||
|
)
|
||||||
|
filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe)
|
||||||
|
dk.data_dictionary["prediction_features"] = filtered_dataframe
|
||||||
|
|
||||||
|
# optional additional data cleaning/analysis
|
||||||
|
self.data_cleaning_predict(dk)
|
||||||
|
|
||||||
|
pred_df = self.rl_model_predict(
|
||||||
|
dk.data_dictionary["prediction_features"], dk, self.model)
|
||||||
|
pred_df.fillna(0, inplace=True)
|
||||||
|
|
||||||
|
return (pred_df, dk.do_predict)
|
||||||
|
|
||||||
|
def rl_model_predict(self, dataframe: DataFrame,
|
||||||
|
dk: FreqaiDataKitchen, model: Any) -> DataFrame:
|
||||||
|
"""
|
||||||
|
A helper function to make predictions in the Reinforcement learning module.
|
||||||
|
:param dataframe: DataFrame = the dataframe of features to make the predictions on
|
||||||
|
:param dk: FreqaiDatakitchen = data kitchen for the current pair
|
||||||
|
:param model: Any = the trained model used to inference the features.
|
||||||
|
"""
|
||||||
|
output = pd.DataFrame(np.zeros(len(dataframe)), columns=dk.label_list)
|
||||||
|
|
||||||
|
def _predict(window):
|
||||||
|
observations = dataframe.iloc[window.index]
|
||||||
|
if self.live and self.rl_config.get('add_state_info', False):
|
||||||
|
market_side, current_profit, trade_duration = self.get_state_info(dk.pair)
|
||||||
|
observations['current_profit_pct'] = current_profit
|
||||||
|
observations['position'] = market_side
|
||||||
|
observations['trade_duration'] = trade_duration
|
||||||
|
res, _ = model.predict(observations, deterministic=True)
|
||||||
|
return res
|
||||||
|
|
||||||
|
output = output.rolling(window=self.CONV_WIDTH).apply(_predict)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
def build_ohlc_price_dataframes(self, data_dictionary: dict,
|
||||||
|
pair: str, dk: FreqaiDataKitchen) -> Tuple[DataFrame,
|
||||||
|
DataFrame]:
|
||||||
|
"""
|
||||||
|
Builds the train prices and test prices for the environment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pair = pair.replace(':', '')
|
||||||
|
train_df = data_dictionary["train_features"]
|
||||||
|
test_df = data_dictionary["test_features"]
|
||||||
|
|
||||||
|
# price data for model training and evaluation
|
||||||
|
tf = self.config['timeframe']
|
||||||
|
ohlc_list = [f'%-{pair}raw_open_{tf}', f'%-{pair}raw_low_{tf}',
|
||||||
|
f'%-{pair}raw_high_{tf}', f'%-{pair}raw_close_{tf}']
|
||||||
|
rename_dict = {f'%-{pair}raw_open_{tf}': 'open', f'%-{pair}raw_low_{tf}': 'low',
|
||||||
|
f'%-{pair}raw_high_{tf}': ' high', f'%-{pair}raw_close_{tf}': 'close'}
|
||||||
|
|
||||||
|
prices_train = train_df.filter(ohlc_list, axis=1)
|
||||||
|
if prices_train.empty:
|
||||||
|
raise OperationalException('Reinforcement learning module didnt find the raw prices '
|
||||||
|
'assigned in populate_any_indicators. Please assign them '
|
||||||
|
'with:\n'
|
||||||
|
'informative[f"%-{pair}raw_close"] = informative["close"]\n'
|
||||||
|
'informative[f"%-{pair}raw_open"] = informative["open"]\n'
|
||||||
|
'informative[f"%-{pair}raw_high"] = informative["high"]\n'
|
||||||
|
'informative[f"%-{pair}raw_low"] = informative["low"]\n')
|
||||||
|
prices_train.rename(columns=rename_dict, inplace=True)
|
||||||
|
prices_train.reset_index(drop=True)
|
||||||
|
|
||||||
|
prices_test = test_df.filter(ohlc_list, axis=1)
|
||||||
|
prices_test.rename(columns=rename_dict, inplace=True)
|
||||||
|
prices_test.reset_index(drop=True)
|
||||||
|
|
||||||
|
return prices_train, prices_test
|
||||||
|
|
||||||
|
def load_model_from_disk(self, dk: FreqaiDataKitchen) -> Any:
|
||||||
|
"""
|
||||||
|
Can be used by user if they are trying to limit_ram_usage *and*
|
||||||
|
perform continual learning.
|
||||||
|
For now, this is unused.
|
||||||
|
"""
|
||||||
|
exists = Path(dk.data_path / f"{dk.model_filename}_model").is_file()
|
||||||
|
if exists:
|
||||||
|
model = self.MODELCLASS.load(dk.data_path / f"{dk.model_filename}_model")
|
||||||
|
else:
|
||||||
|
logger.info('No model file on disk to continue learning from.')
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
def _on_stop(self):
|
||||||
|
"""
|
||||||
|
Hook called on bot shutdown. Close SubprocVecEnv subprocesses for clean shutdown.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.train_env:
|
||||||
|
self.train_env.close()
|
||||||
|
|
||||||
|
if self.eval_env:
|
||||||
|
self.eval_env.close()
|
||||||
|
|
||||||
|
# Nested class which can be overridden by user to customize further
|
||||||
|
class MyRLEnv(Base5ActionRLEnv):
|
||||||
|
"""
|
||||||
|
User can override any function in BaseRLEnv and gym.Env. Here the user
|
||||||
|
sets a custom reward based on profit and trade duration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def calculate_reward(self, action: int) -> float:
|
||||||
|
"""
|
||||||
|
An example reward function. This is the one function that users will likely
|
||||||
|
wish to inject their own creativity into.
|
||||||
|
:param action: int = The action made by the agent for the current candle.
|
||||||
|
:return:
|
||||||
|
float = the reward to give to the agent for current step (used for optimization
|
||||||
|
of weights in NN)
|
||||||
|
"""
|
||||||
|
# first, penalize if the action is not valid
|
||||||
|
if not self._is_valid(action):
|
||||||
|
return -2
|
||||||
|
|
||||||
|
pnl = self.get_unrealized_profit()
|
||||||
|
factor = 100.
|
||||||
|
|
||||||
|
# reward agent for entering trades
|
||||||
|
if (action in (Actions.Long_enter.value, Actions.Short_enter.value)
|
||||||
|
and self._position == Positions.Neutral):
|
||||||
|
return 25
|
||||||
|
# discourage agent from not entering trades
|
||||||
|
if action == Actions.Neutral.value and self._position == Positions.Neutral:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
|
||||||
|
if self._last_trade_tick:
|
||||||
|
trade_duration = self._current_tick - self._last_trade_tick
|
||||||
|
else:
|
||||||
|
trade_duration = 0
|
||||||
|
|
||||||
|
if trade_duration <= max_trade_duration:
|
||||||
|
factor *= 1.5
|
||||||
|
elif trade_duration > max_trade_duration:
|
||||||
|
factor *= 0.5
|
||||||
|
|
||||||
|
# discourage sitting in position
|
||||||
|
if (self._position in (Positions.Short, Positions.Long) and
|
||||||
|
action == Actions.Neutral.value):
|
||||||
|
return -1 * trade_duration / max_trade_duration
|
||||||
|
|
||||||
|
# close long
|
||||||
|
if action == Actions.Long_exit.value and self._position == Positions.Long:
|
||||||
|
if pnl > self.profit_aim * self.rr:
|
||||||
|
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||||
|
return float(pnl * factor)
|
||||||
|
|
||||||
|
# close short
|
||||||
|
if action == Actions.Short_exit.value and self._position == Positions.Short:
|
||||||
|
if pnl > self.profit_aim * self.rr:
|
||||||
|
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||||
|
return float(pnl * factor)
|
||||||
|
|
||||||
|
return 0.
|
||||||
|
|
||||||
|
|
||||||
|
def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int,
|
||||||
|
seed: int, train_df: DataFrame, price: DataFrame,
|
||||||
|
reward_params: Dict[str, int], window_size: int, monitor: bool = False,
|
||||||
|
config: Dict[str, Any] = {}) -> Callable:
|
||||||
|
"""
|
||||||
|
Utility function for multiprocessed env.
|
||||||
|
|
||||||
|
:param env_id: (str) the environment ID
|
||||||
|
:param num_env: (int) the number of environment you wish to have in subprocesses
|
||||||
|
:param seed: (int) the inital seed for RNG
|
||||||
|
:param rank: (int) index of the subprocess
|
||||||
|
:return: (Callable)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _init() -> gym.Env:
|
||||||
|
|
||||||
|
env = MyRLEnv(df=train_df, prices=price, window_size=window_size,
|
||||||
|
reward_kwargs=reward_params, id=env_id, seed=seed + rank, config=config)
|
||||||
|
if monitor:
|
||||||
|
env = Monitor(env)
|
||||||
|
return env
|
||||||
|
set_random_seed(seed)
|
||||||
|
return _init
|
0
freqtrade/freqai/RL/__init__.py
Normal file
0
freqtrade/freqai/RL/__init__.py
Normal file
@ -1,9 +1,10 @@
|
|||||||
import collections
|
import collections
|
||||||
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import threading
|
import threading
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Tuple, TypedDict
|
from typing import Any, Dict, Tuple, TypedDict
|
||||||
|
|
||||||
@ -81,6 +82,7 @@ class FreqaiDataDrawer:
|
|||||||
self.historic_predictions_bkp_path = Path(
|
self.historic_predictions_bkp_path = Path(
|
||||||
self.full_path / "historic_predictions.backup.pkl")
|
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.metric_tracker_path = Path(self.full_path / "metric_tracker.json")
|
self.metric_tracker_path = Path(self.full_path / "metric_tracker.json")
|
||||||
self.follow_mode = follow_mode
|
self.follow_mode = follow_mode
|
||||||
if follow_mode:
|
if follow_mode:
|
||||||
@ -98,6 +100,7 @@ class FreqaiDataDrawer:
|
|||||||
self.empty_pair_dict: pair_info = {
|
self.empty_pair_dict: pair_info = {
|
||||||
"model_filename": "", "trained_timestamp": 0,
|
"model_filename": "", "trained_timestamp": 0,
|
||||||
"data_path": "", "extras": {}}
|
"data_path": "", "extras": {}}
|
||||||
|
self.model_type = self.freqai_info.get('model_save_type', 'joblib')
|
||||||
|
|
||||||
def update_metric_tracker(self, metric: str, value: float, pair: str) -> None:
|
def update_metric_tracker(self, metric: str, value: float, pair: str) -> None:
|
||||||
"""
|
"""
|
||||||
@ -125,6 +128,17 @@ class FreqaiDataDrawer:
|
|||||||
self.update_metric_tracker('cpu_load5min', load5 / cpus, pair)
|
self.update_metric_tracker('cpu_load5min', load5 / cpus, pair)
|
||||||
self.update_metric_tracker('cpu_load15min', load15 / cpus, pair)
|
self.update_metric_tracker('cpu_load15min', load15 / cpus, pair)
|
||||||
|
|
||||||
|
def load_global_metadata_from_disk(self):
|
||||||
|
"""
|
||||||
|
Locate and load a previously saved global metadata in present model folder.
|
||||||
|
"""
|
||||||
|
exists = self.global_metadata_path.is_file()
|
||||||
|
if exists:
|
||||||
|
with open(self.global_metadata_path, "r") as fp:
|
||||||
|
metatada_dict = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
||||||
|
return metatada_dict
|
||||||
|
return {}
|
||||||
|
|
||||||
def load_drawer_from_disk(self):
|
def load_drawer_from_disk(self):
|
||||||
"""
|
"""
|
||||||
Locate and load a previously saved data drawer full of all pair model metadata in
|
Locate and load a previously saved data drawer full of all pair model metadata in
|
||||||
@ -225,6 +239,15 @@ class FreqaiDataDrawer:
|
|||||||
rapidjson.dump(self.follower_dict, fp, default=self.np_encoder,
|
rapidjson.dump(self.follower_dict, fp, default=self.np_encoder,
|
||||||
number_mode=rapidjson.NM_NATIVE)
|
number_mode=rapidjson.NM_NATIVE)
|
||||||
|
|
||||||
|
def save_global_metadata_to_disk(self, metadata: Dict[str, Any]):
|
||||||
|
"""
|
||||||
|
Save global metadata json to disk
|
||||||
|
"""
|
||||||
|
with self.save_lock:
|
||||||
|
with open(self.global_metadata_path, 'w') as fp:
|
||||||
|
rapidjson.dump(metadata, fp, default=self.np_encoder,
|
||||||
|
number_mode=rapidjson.NM_NATIVE)
|
||||||
|
|
||||||
def create_follower_dict(self):
|
def create_follower_dict(self):
|
||||||
"""
|
"""
|
||||||
Create or dictionary for each follower to maintain unique persistent prediction targets
|
Create or dictionary for each follower to maintain unique persistent prediction targets
|
||||||
@ -476,10 +499,12 @@ class FreqaiDataDrawer:
|
|||||||
save_path = Path(dk.data_path)
|
save_path = Path(dk.data_path)
|
||||||
|
|
||||||
# Save the trained model
|
# Save the trained model
|
||||||
if not dk.keras:
|
if self.model_type == 'joblib':
|
||||||
dump(model, save_path / f"{dk.model_filename}_model.joblib")
|
dump(model, save_path / f"{dk.model_filename}_model.joblib")
|
||||||
else:
|
elif self.model_type == 'keras':
|
||||||
model.save(save_path / f"{dk.model_filename}_model.h5")
|
model.save(save_path / f"{dk.model_filename}_model.h5")
|
||||||
|
elif 'stable_baselines' in self.model_type:
|
||||||
|
model.save(save_path / f"{dk.model_filename}_model.zip")
|
||||||
|
|
||||||
if dk.svm_model is not None:
|
if dk.svm_model is not None:
|
||||||
dump(dk.svm_model, save_path / f"{dk.model_filename}_svm_model.joblib")
|
dump(dk.svm_model, save_path / f"{dk.model_filename}_svm_model.joblib")
|
||||||
@ -506,11 +531,10 @@ class FreqaiDataDrawer:
|
|||||||
dk.pca, open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "wb")
|
dk.pca, open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "wb")
|
||||||
)
|
)
|
||||||
|
|
||||||
# if self.live:
|
|
||||||
# store as much in ram as possible to increase performance
|
|
||||||
self.model_dictionary[coin] = model
|
self.model_dictionary[coin] = model
|
||||||
self.pair_dict[coin]["model_filename"] = dk.model_filename
|
self.pair_dict[coin]["model_filename"] = dk.model_filename
|
||||||
self.pair_dict[coin]["data_path"] = str(dk.data_path)
|
self.pair_dict[coin]["data_path"] = str(dk.data_path)
|
||||||
|
|
||||||
if coin not in self.meta_data_dictionary:
|
if coin not in self.meta_data_dictionary:
|
||||||
self.meta_data_dictionary[coin] = {}
|
self.meta_data_dictionary[coin] = {}
|
||||||
self.meta_data_dictionary[coin]["train_df"] = dk.data_dictionary["train_features"]
|
self.meta_data_dictionary[coin]["train_df"] = dk.data_dictionary["train_features"]
|
||||||
@ -542,14 +566,6 @@ class FreqaiDataDrawer:
|
|||||||
if dk.live:
|
if dk.live:
|
||||||
dk.model_filename = self.pair_dict[coin]["model_filename"]
|
dk.model_filename = self.pair_dict[coin]["model_filename"]
|
||||||
dk.data_path = Path(self.pair_dict[coin]["data_path"])
|
dk.data_path = Path(self.pair_dict[coin]["data_path"])
|
||||||
if self.freqai_info.get("follow_mode", False):
|
|
||||||
# follower can be on a different system which is rsynced from the leader:
|
|
||||||
dk.data_path = Path(
|
|
||||||
self.config["user_data_dir"]
|
|
||||||
/ "models"
|
|
||||||
/ dk.data_path.parts[-2]
|
|
||||||
/ dk.data_path.parts[-1]
|
|
||||||
)
|
|
||||||
|
|
||||||
if coin in self.meta_data_dictionary:
|
if coin in self.meta_data_dictionary:
|
||||||
dk.data = self.meta_data_dictionary[coin]["meta_data"]
|
dk.data = self.meta_data_dictionary[coin]["meta_data"]
|
||||||
@ -568,12 +584,16 @@ class FreqaiDataDrawer:
|
|||||||
# try to access model in memory instead of loading object from disk to save time
|
# try to access model in memory instead of loading object from disk to save time
|
||||||
if dk.live and coin in self.model_dictionary:
|
if dk.live and coin in self.model_dictionary:
|
||||||
model = self.model_dictionary[coin]
|
model = self.model_dictionary[coin]
|
||||||
elif not dk.keras:
|
elif self.model_type == 'joblib':
|
||||||
model = load(dk.data_path / f"{dk.model_filename}_model.joblib")
|
model = load(dk.data_path / f"{dk.model_filename}_model.joblib")
|
||||||
else:
|
elif self.model_type == 'keras':
|
||||||
from tensorflow import keras
|
from tensorflow import keras
|
||||||
|
|
||||||
model = keras.models.load_model(dk.data_path / f"{dk.model_filename}_model.h5")
|
model = keras.models.load_model(dk.data_path / f"{dk.model_filename}_model.h5")
|
||||||
|
elif self.model_type == 'stable_baselines':
|
||||||
|
mod = importlib.import_module(
|
||||||
|
'stable_baselines3', self.freqai_info['rl_config']['model_type'])
|
||||||
|
MODELCLASS = getattr(mod, self.freqai_info['rl_config']['model_type'])
|
||||||
|
model = MODELCLASS.load(dk.data_path / f"{dk.model_filename}_model")
|
||||||
|
|
||||||
if Path(dk.data_path / f"{dk.model_filename}_svm_model.joblib").is_file():
|
if Path(dk.data_path / f"{dk.model_filename}_svm_model.joblib").is_file():
|
||||||
dk.svm_model = load(dk.data_path / f"{dk.model_filename}_svm_model.joblib")
|
dk.svm_model = load(dk.data_path / f"{dk.model_filename}_svm_model.joblib")
|
||||||
@ -583,6 +603,10 @@ class FreqaiDataDrawer:
|
|||||||
f"Unable to load model, ensure model exists at " f"{dk.data_path} "
|
f"Unable to load model, ensure model exists at " f"{dk.data_path} "
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# load it into ram if it was loaded from disk
|
||||||
|
if coin not in self.model_dictionary:
|
||||||
|
self.model_dictionary[coin] = model
|
||||||
|
|
||||||
if self.config["freqai"]["feature_parameters"]["principal_component_analysis"]:
|
if self.config["freqai"]["feature_parameters"]["principal_component_analysis"]:
|
||||||
dk.pca = cloudpickle.load(
|
dk.pca = cloudpickle.load(
|
||||||
open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "rb")
|
open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "rb")
|
||||||
@ -693,3 +717,31 @@ class FreqaiDataDrawer:
|
|||||||
).reset_index(drop=True)
|
).reset_index(drop=True)
|
||||||
|
|
||||||
return corr_dataframes, base_dataframes
|
return corr_dataframes, base_dataframes
|
||||||
|
|
||||||
|
def get_timerange_from_live_historic_predictions(self) -> TimeRange:
|
||||||
|
"""
|
||||||
|
Returns timerange information based on historic predictions file
|
||||||
|
:return: timerange calculated from saved live data
|
||||||
|
"""
|
||||||
|
if not self.historic_predictions_path.is_file():
|
||||||
|
raise OperationalException(
|
||||||
|
'Historic predictions not found. Historic predictions data is required '
|
||||||
|
'to run backtest with the freqai-backtest-live-models option '
|
||||||
|
)
|
||||||
|
|
||||||
|
self.load_historic_predictions_from_disk()
|
||||||
|
|
||||||
|
all_pairs_end_dates = []
|
||||||
|
for pair in self.historic_predictions:
|
||||||
|
pair_historic_data = self.historic_predictions[pair]
|
||||||
|
all_pairs_end_dates.append(pair_historic_data.date_pred.max())
|
||||||
|
|
||||||
|
global_metadata = self.load_global_metadata_from_disk()
|
||||||
|
start_date = datetime.fromtimestamp(int(global_metadata["start_dry_live_date"]))
|
||||||
|
end_date = max(all_pairs_end_dates)
|
||||||
|
# add 1 day to string timerange to ensure BT module will load all dataframe data
|
||||||
|
end_date = end_date + timedelta(days=1)
|
||||||
|
backtesting_timerange = TimeRange(
|
||||||
|
'date', 'date', int(start_date.timestamp()), int(end_date.timestamp())
|
||||||
|
)
|
||||||
|
return backtesting_timerange
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timezone
|
||||||
from math import cos, sin
|
from math import cos, sin
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Tuple
|
from typing import Any, Dict, List, Tuple
|
||||||
@ -9,6 +9,7 @@ from typing import Any, Dict, List, Tuple
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import numpy.typing as npt
|
import numpy.typing as npt
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import psutil
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from scipy import stats
|
from scipy import stats
|
||||||
from sklearn import linear_model
|
from sklearn import linear_model
|
||||||
@ -86,12 +87,7 @@ class FreqaiDataKitchen:
|
|||||||
if not self.live:
|
if not self.live:
|
||||||
self.full_path = self.get_full_models_path(self.config)
|
self.full_path = self.get_full_models_path(self.config)
|
||||||
|
|
||||||
if self.backtest_live_models:
|
if not self.backtest_live_models:
|
||||||
if self.pair:
|
|
||||||
self.set_timerange_from_ready_models()
|
|
||||||
(self.training_timeranges,
|
|
||||||
self.backtesting_timeranges) = self.split_timerange_live_models()
|
|
||||||
else:
|
|
||||||
self.full_timerange = self.create_fulltimerange(
|
self.full_timerange = self.create_fulltimerange(
|
||||||
self.config["timerange"], self.freqai_config.get("train_period_days", 0)
|
self.config["timerange"], self.freqai_config.get("train_period_days", 0)
|
||||||
)
|
)
|
||||||
@ -102,7 +98,10 @@ class FreqaiDataKitchen:
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.data['extra_returns_per_train'] = self.freqai_config.get('extra_returns_per_train', {})
|
self.data['extra_returns_per_train'] = self.freqai_config.get('extra_returns_per_train', {})
|
||||||
self.thread_count = self.freqai_config.get("data_kitchen_thread_count", -1)
|
if not self.freqai_config.get("data_kitchen_thread_count", 0):
|
||||||
|
self.thread_count = max(int(psutil.cpu_count() * 2 - 2), 1)
|
||||||
|
else:
|
||||||
|
self.thread_count = self.freqai_config["data_kitchen_thread_count"]
|
||||||
self.train_dates: DataFrame = pd.DataFrame()
|
self.train_dates: DataFrame = pd.DataFrame()
|
||||||
self.unique_classes: Dict[str, list] = {}
|
self.unique_classes: Dict[str, list] = {}
|
||||||
self.unique_class_list: list = []
|
self.unique_class_list: list = []
|
||||||
@ -433,9 +432,7 @@ class FreqaiDataKitchen:
|
|||||||
timerange_train.stopts = timerange_train.startts + train_period_days
|
timerange_train.stopts = timerange_train.startts + train_period_days
|
||||||
|
|
||||||
first = False
|
first = False
|
||||||
start = datetime.fromtimestamp(timerange_train.startts, tz=timezone.utc)
|
tr_training_list.append(timerange_train.timerange_str)
|
||||||
stop = datetime.fromtimestamp(timerange_train.stopts, tz=timezone.utc)
|
|
||||||
tr_training_list.append(start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d"))
|
|
||||||
tr_training_list_timerange.append(copy.deepcopy(timerange_train))
|
tr_training_list_timerange.append(copy.deepcopy(timerange_train))
|
||||||
|
|
||||||
# associated backtest period
|
# associated backtest period
|
||||||
@ -447,9 +444,7 @@ class FreqaiDataKitchen:
|
|||||||
if timerange_backtest.stopts > config_timerange.stopts:
|
if timerange_backtest.stopts > config_timerange.stopts:
|
||||||
timerange_backtest.stopts = config_timerange.stopts
|
timerange_backtest.stopts = config_timerange.stopts
|
||||||
|
|
||||||
start = datetime.fromtimestamp(timerange_backtest.startts, tz=timezone.utc)
|
tr_backtesting_list.append(timerange_backtest.timerange_str)
|
||||||
stop = datetime.fromtimestamp(timerange_backtest.stopts, tz=timezone.utc)
|
|
||||||
tr_backtesting_list.append(start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d"))
|
|
||||||
tr_backtesting_list_timerange.append(copy.deepcopy(timerange_backtest))
|
tr_backtesting_list_timerange.append(copy.deepcopy(timerange_backtest))
|
||||||
|
|
||||||
# ensure we are predicting on exactly same amount of data as requested by user defined
|
# ensure we are predicting on exactly same amount of data as requested by user defined
|
||||||
@ -460,29 +455,6 @@ class FreqaiDataKitchen:
|
|||||||
# print(tr_training_list, tr_backtesting_list)
|
# print(tr_training_list, tr_backtesting_list)
|
||||||
return tr_training_list_timerange, tr_backtesting_list_timerange
|
return tr_training_list_timerange, tr_backtesting_list_timerange
|
||||||
|
|
||||||
def split_timerange_live_models(
|
|
||||||
self
|
|
||||||
) -> Tuple[list, list]:
|
|
||||||
|
|
||||||
tr_backtesting_list_timerange = []
|
|
||||||
asset = self.pair.split("/")[0]
|
|
||||||
if asset not in self.backtest_live_models_data["assets_end_dates"]:
|
|
||||||
raise OperationalException(
|
|
||||||
f"Model not available for pair {self.pair}. "
|
|
||||||
"Please, try again after removing this pair from the configuration file."
|
|
||||||
)
|
|
||||||
asset_data = self.backtest_live_models_data["assets_end_dates"][asset]
|
|
||||||
backtesting_timerange = self.backtest_live_models_data["backtesting_timerange"]
|
|
||||||
model_end_dates = [x for x in asset_data]
|
|
||||||
model_end_dates.append(backtesting_timerange.stopts)
|
|
||||||
model_end_dates.sort()
|
|
||||||
for index, item in enumerate(model_end_dates):
|
|
||||||
if len(model_end_dates) > (index + 1):
|
|
||||||
tr_to_add = TimeRange("date", "date", item, model_end_dates[index + 1])
|
|
||||||
tr_backtesting_list_timerange.append(tr_to_add)
|
|
||||||
|
|
||||||
return tr_backtesting_list_timerange, tr_backtesting_list_timerange
|
|
||||||
|
|
||||||
def slice_dataframe(self, timerange: TimeRange, df: DataFrame) -> DataFrame:
|
def slice_dataframe(self, timerange: TimeRange, df: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Given a full dataframe, extract the user desired window
|
Given a full dataframe, extract the user desired window
|
||||||
@ -491,11 +463,9 @@ class FreqaiDataKitchen:
|
|||||||
it is sliced down to just the present training period.
|
it is sliced down to just the present training period.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
|
df = df.loc[df["date"] >= timerange.startdt, :]
|
||||||
stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
|
|
||||||
df = df.loc[df["date"] >= start, :]
|
|
||||||
if not self.live:
|
if not self.live:
|
||||||
df = df.loc[df["date"] < stop, :]
|
df = df.loc[df["date"] < timerange.stopdt, :]
|
||||||
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
@ -980,7 +950,8 @@ class FreqaiDataKitchen:
|
|||||||
return weights
|
return weights
|
||||||
|
|
||||||
def get_predictions_to_append(self, predictions: DataFrame,
|
def get_predictions_to_append(self, predictions: DataFrame,
|
||||||
do_predict: npt.ArrayLike) -> DataFrame:
|
do_predict: npt.ArrayLike,
|
||||||
|
dataframe_backtest: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Get backtest prediction from current backtest period
|
Get backtest prediction from current backtest period
|
||||||
"""
|
"""
|
||||||
@ -1002,7 +973,9 @@ class FreqaiDataKitchen:
|
|||||||
if self.freqai_config["feature_parameters"].get("DI_threshold", 0) > 0:
|
if self.freqai_config["feature_parameters"].get("DI_threshold", 0) > 0:
|
||||||
append_df["DI_values"] = self.DI_values
|
append_df["DI_values"] = self.DI_values
|
||||||
|
|
||||||
return append_df
|
dataframe_backtest.reset_index(drop=True, inplace=True)
|
||||||
|
merged_df = pd.concat([dataframe_backtest["date"], append_df], axis=1)
|
||||||
|
return merged_df
|
||||||
|
|
||||||
def append_predictions(self, append_df: DataFrame) -> None:
|
def append_predictions(self, append_df: DataFrame) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1012,23 +985,18 @@ class FreqaiDataKitchen:
|
|||||||
if self.full_df.empty:
|
if self.full_df.empty:
|
||||||
self.full_df = append_df
|
self.full_df = append_df
|
||||||
else:
|
else:
|
||||||
self.full_df = pd.concat([self.full_df, append_df], axis=0)
|
self.full_df = pd.concat([self.full_df, append_df], axis=0, ignore_index=True)
|
||||||
|
|
||||||
def fill_predictions(self, dataframe):
|
def fill_predictions(self, dataframe):
|
||||||
"""
|
"""
|
||||||
Back fill values to before the backtesting range so that the dataframe matches size
|
Back fill values to before the backtesting range so that the dataframe matches size
|
||||||
when it goes back to the strategy. These rows are not included in the backtest.
|
when it goes back to the strategy. These rows are not included in the backtest.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
len_filler = len(dataframe) - len(self.full_df.index) # startup_candle_count
|
|
||||||
filler_df = pd.DataFrame(
|
|
||||||
np.zeros((len_filler, len(self.full_df.columns))), columns=self.full_df.columns
|
|
||||||
)
|
|
||||||
|
|
||||||
self.full_df = pd.concat([filler_df, self.full_df], axis=0, ignore_index=True)
|
|
||||||
|
|
||||||
to_keep = [col for col in dataframe.columns if not col.startswith("&")]
|
to_keep = [col for col in dataframe.columns if not col.startswith("&")]
|
||||||
self.return_dataframe = pd.concat([dataframe[to_keep], self.full_df], axis=1)
|
self.return_dataframe = pd.merge(dataframe[to_keep],
|
||||||
|
self.full_df, how='left', on='date')
|
||||||
|
self.return_dataframe[self.full_df.columns] = (
|
||||||
|
self.return_dataframe[self.full_df.columns].fillna(value=0))
|
||||||
self.full_df = DataFrame()
|
self.full_df = DataFrame()
|
||||||
|
|
||||||
return
|
return
|
||||||
@ -1058,9 +1026,7 @@ class FreqaiDataKitchen:
|
|||||||
backtest_timerange.startts = (
|
backtest_timerange.startts = (
|
||||||
backtest_timerange.startts - backtest_period_days * SECONDS_IN_DAY
|
backtest_timerange.startts - backtest_period_days * SECONDS_IN_DAY
|
||||||
)
|
)
|
||||||
start = datetime.fromtimestamp(backtest_timerange.startts, tz=timezone.utc)
|
full_timerange = backtest_timerange.timerange_str
|
||||||
stop = datetime.fromtimestamp(backtest_timerange.stopts, tz=timezone.utc)
|
|
||||||
full_timerange = start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d")
|
|
||||||
config_path = Path(self.config["config_files"][0])
|
config_path = Path(self.config["config_files"][0])
|
||||||
|
|
||||||
if not self.full_path.is_dir():
|
if not self.full_path.is_dir():
|
||||||
@ -1327,22 +1293,22 @@ class FreqaiDataKitchen:
|
|||||||
self, append_df: DataFrame
|
self, append_df: DataFrame
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Save prediction dataframe from backtesting to h5 file format
|
Save prediction dataframe from backtesting to feather file format
|
||||||
:param append_df: dataframe for backtesting period
|
:param append_df: dataframe for backtesting period
|
||||||
"""
|
"""
|
||||||
full_predictions_folder = Path(self.full_path / self.backtest_predictions_folder)
|
full_predictions_folder = Path(self.full_path / self.backtest_predictions_folder)
|
||||||
if not full_predictions_folder.is_dir():
|
if not full_predictions_folder.is_dir():
|
||||||
full_predictions_folder.mkdir(parents=True, exist_ok=True)
|
full_predictions_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
append_df.to_hdf(self.backtesting_results_path, key='append_df', mode='w')
|
append_df.to_feather(self.backtesting_results_path)
|
||||||
|
|
||||||
def get_backtesting_prediction(
|
def get_backtesting_prediction(
|
||||||
self
|
self
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Get prediction dataframe from h5 file format
|
Get prediction dataframe from feather file format
|
||||||
"""
|
"""
|
||||||
append_df = pd.read_hdf(self.backtesting_results_path)
|
append_df = pd.read_feather(self.backtesting_results_path)
|
||||||
return append_df
|
return append_df
|
||||||
|
|
||||||
def check_if_backtest_prediction_is_valid(
|
def check_if_backtest_prediction_is_valid(
|
||||||
@ -1358,19 +1324,20 @@ class FreqaiDataKitchen:
|
|||||||
"""
|
"""
|
||||||
path_to_predictionfile = Path(self.full_path /
|
path_to_predictionfile = Path(self.full_path /
|
||||||
self.backtest_predictions_folder /
|
self.backtest_predictions_folder /
|
||||||
f"{self.model_filename}_prediction.h5")
|
f"{self.model_filename}_prediction.feather")
|
||||||
self.backtesting_results_path = path_to_predictionfile
|
self.backtesting_results_path = path_to_predictionfile
|
||||||
|
|
||||||
file_exists = path_to_predictionfile.is_file()
|
file_exists = path_to_predictionfile.is_file()
|
||||||
|
|
||||||
if file_exists:
|
if file_exists:
|
||||||
append_df = self.get_backtesting_prediction()
|
append_df = self.get_backtesting_prediction()
|
||||||
if len(append_df) == len_backtest_df:
|
if len(append_df) == len_backtest_df and 'date' in append_df:
|
||||||
logger.info(f"Found backtesting prediction file at {path_to_predictionfile}")
|
logger.info(f"Found backtesting prediction file at {path_to_predictionfile}")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.info("A new backtesting prediction file is required. "
|
logger.info("A new backtesting prediction file is required. "
|
||||||
"(Number of predictions is different from dataframe length).")
|
"(Number of predictions is different from dataframe length or "
|
||||||
|
"old prediction file version).")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -1378,17 +1345,6 @@ class FreqaiDataKitchen:
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def set_timerange_from_ready_models(self):
|
|
||||||
backtesting_timerange, \
|
|
||||||
assets_end_dates = (
|
|
||||||
self.get_timerange_and_assets_end_dates_from_ready_models(self.full_path))
|
|
||||||
|
|
||||||
self.backtest_live_models_data = {
|
|
||||||
"backtesting_timerange": backtesting_timerange,
|
|
||||||
"assets_end_dates": assets_end_dates
|
|
||||||
}
|
|
||||||
return
|
|
||||||
|
|
||||||
def get_full_models_path(self, config: Config) -> Path:
|
def get_full_models_path(self, config: Config) -> Path:
|
||||||
"""
|
"""
|
||||||
Returns default FreqAI model path
|
Returns default FreqAI model path
|
||||||
@ -1399,88 +1355,6 @@ class FreqaiDataKitchen:
|
|||||||
config["user_data_dir"] / "models" / str(freqai_config.get("identifier"))
|
config["user_data_dir"] / "models" / str(freqai_config.get("identifier"))
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_timerange_and_assets_end_dates_from_ready_models(
|
|
||||||
self, models_path: Path) -> Tuple[TimeRange, Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Returns timerange information based on a FreqAI model directory
|
|
||||||
:param models_path: FreqAI model path
|
|
||||||
|
|
||||||
:return: a Tuple with (Timerange calculated from directory and
|
|
||||||
a Dict with pair and model end training dates info)
|
|
||||||
"""
|
|
||||||
all_models_end_dates = []
|
|
||||||
assets_end_dates: Dict[str, Any] = self.get_assets_timestamps_training_from_ready_models(
|
|
||||||
models_path)
|
|
||||||
for key in assets_end_dates:
|
|
||||||
for model_end_date in assets_end_dates[key]:
|
|
||||||
if model_end_date not in all_models_end_dates:
|
|
||||||
all_models_end_dates.append(model_end_date)
|
|
||||||
|
|
||||||
if len(all_models_end_dates) == 0:
|
|
||||||
raise OperationalException(
|
|
||||||
'At least 1 saved model is required to '
|
|
||||||
'run backtest with the freqai-backtest-live-models option'
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(all_models_end_dates) == 1:
|
|
||||||
logger.warning(
|
|
||||||
"Only 1 model was found. Backtesting will run with the "
|
|
||||||
"timerange from the end of the training date to the current date"
|
|
||||||
)
|
|
||||||
|
|
||||||
finish_timestamp = int(datetime.now(tz=timezone.utc).timestamp())
|
|
||||||
if len(all_models_end_dates) > 1:
|
|
||||||
# After last model end date, use the same period from previous model
|
|
||||||
# to finish the backtest
|
|
||||||
all_models_end_dates.sort(reverse=True)
|
|
||||||
finish_timestamp = all_models_end_dates[0] + \
|
|
||||||
(all_models_end_dates[0] - all_models_end_dates[1])
|
|
||||||
|
|
||||||
all_models_end_dates.append(finish_timestamp)
|
|
||||||
all_models_end_dates.sort()
|
|
||||||
start_date = (datetime(*datetime.fromtimestamp(min(all_models_end_dates),
|
|
||||||
timezone.utc).timetuple()[:3], tzinfo=timezone.utc))
|
|
||||||
end_date = (datetime(*datetime.fromtimestamp(max(all_models_end_dates),
|
|
||||||
timezone.utc).timetuple()[:3], tzinfo=timezone.utc))
|
|
||||||
|
|
||||||
# add 1 day to string timerange to ensure BT module will load all dataframe data
|
|
||||||
end_date = end_date + timedelta(days=1)
|
|
||||||
backtesting_timerange = TimeRange(
|
|
||||||
'date', 'date', int(start_date.timestamp()), int(end_date.timestamp())
|
|
||||||
)
|
|
||||||
return backtesting_timerange, assets_end_dates
|
|
||||||
|
|
||||||
def get_assets_timestamps_training_from_ready_models(
|
|
||||||
self, models_path: Path) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Scan the models path and returns all assets end training dates (timestamp)
|
|
||||||
:param models_path: FreqAI model path
|
|
||||||
|
|
||||||
:return: a Dict with asset and model end training dates info
|
|
||||||
"""
|
|
||||||
assets_end_dates: Dict[str, Any] = {}
|
|
||||||
if not models_path.is_dir():
|
|
||||||
raise OperationalException(
|
|
||||||
'Model folders not found. Saved models are required '
|
|
||||||
'to run backtest with the freqai-backtest-live-models option'
|
|
||||||
)
|
|
||||||
for model_dir in models_path.iterdir():
|
|
||||||
if str(model_dir.name).startswith("sub-train"):
|
|
||||||
model_end_date = int(model_dir.name.split("_")[1])
|
|
||||||
asset = model_dir.name.split("_")[0].replace("sub-train-", "")
|
|
||||||
model_file_name = (
|
|
||||||
f"cb_{str(model_dir.name).replace('sub-train-', '').lower()}"
|
|
||||||
"_model.joblib"
|
|
||||||
)
|
|
||||||
|
|
||||||
model_path_file = Path(model_dir / model_file_name)
|
|
||||||
if model_path_file.is_file():
|
|
||||||
if asset not in assets_end_dates:
|
|
||||||
assets_end_dates[asset] = []
|
|
||||||
assets_end_dates[asset].append(model_end_date)
|
|
||||||
|
|
||||||
return assets_end_dates
|
|
||||||
|
|
||||||
def remove_special_chars_from_feature_names(self, dataframe: pd.DataFrame) -> pd.DataFrame:
|
def remove_special_chars_from_feature_names(self, dataframe: pd.DataFrame) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Remove all special characters from feature strings (:)
|
Remove all special characters from feature strings (:)
|
||||||
|
@ -5,15 +5,17 @@ from abc import ABC, abstractmethod
|
|||||||
from collections import deque
|
from collections import deque
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Literal, Tuple
|
from typing import Any, Dict, List, Literal, Optional, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import psutil
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config
|
from freqtrade.constants import Config
|
||||||
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_seconds
|
||||||
@ -67,6 +69,7 @@ class IFreqaiModel(ABC):
|
|||||||
self.save_backtest_models: bool = self.freqai_info.get("save_backtest_models", True)
|
self.save_backtest_models: bool = self.freqai_info.get("save_backtest_models", True)
|
||||||
if self.save_backtest_models:
|
if self.save_backtest_models:
|
||||||
logger.info('Backtesting module configured to save all models.')
|
logger.info('Backtesting module configured to save all models.')
|
||||||
|
|
||||||
self.dd = FreqaiDataDrawer(Path(self.full_path), self.config, self.follow_mode)
|
self.dd = FreqaiDataDrawer(Path(self.full_path), self.config, self.follow_mode)
|
||||||
# set current candle to arbitrary historical date
|
# set current candle to arbitrary historical date
|
||||||
self.current_candle: datetime = datetime.fromtimestamp(637887600, tz=timezone.utc)
|
self.current_candle: datetime = datetime.fromtimestamp(637887600, tz=timezone.utc)
|
||||||
@ -98,6 +101,9 @@ class IFreqaiModel(ABC):
|
|||||||
self.get_corr_dataframes: bool = True
|
self.get_corr_dataframes: bool = True
|
||||||
self._threads: List[threading.Thread] = []
|
self._threads: List[threading.Thread] = []
|
||||||
self._stop_event = threading.Event()
|
self._stop_event = threading.Event()
|
||||||
|
self.metadata: Dict[str, Any] = self.dd.load_global_metadata_from_disk()
|
||||||
|
self.data_provider: Optional[DataProvider] = None
|
||||||
|
self.max_system_threads = max(int(psutil.cpu_count() * 2 - 2), 1)
|
||||||
|
|
||||||
record_params(config, self.full_path)
|
record_params(config, self.full_path)
|
||||||
|
|
||||||
@ -126,11 +132,13 @@ class IFreqaiModel(ABC):
|
|||||||
|
|
||||||
self.live = strategy.dp.runmode in (RunMode.DRY_RUN, RunMode.LIVE)
|
self.live = strategy.dp.runmode in (RunMode.DRY_RUN, RunMode.LIVE)
|
||||||
self.dd.set_pair_dict_info(metadata)
|
self.dd.set_pair_dict_info(metadata)
|
||||||
|
self.data_provider = strategy.dp
|
||||||
|
|
||||||
if self.live:
|
if self.live:
|
||||||
self.inference_timer('start')
|
self.inference_timer('start')
|
||||||
self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"])
|
self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"])
|
||||||
dk = self.start_live(dataframe, metadata, strategy, self.dk)
|
dk = self.start_live(dataframe, metadata, strategy, self.dk)
|
||||||
|
dataframe = dk.remove_features_from_df(dk.return_dataframe)
|
||||||
|
|
||||||
# For backtesting, each pair enters and then gets trained for each window along the
|
# For backtesting, each pair enters and then gets trained for each window along the
|
||||||
# sliding window defined by "train_period_days" (training window) and "live_retrain_hours"
|
# sliding window defined by "train_period_days" (training window) and "live_retrain_hours"
|
||||||
@ -139,20 +147,24 @@ class IFreqaiModel(ABC):
|
|||||||
# the concatenated results for the full backtesting period back to the strategy.
|
# the concatenated results for the full backtesting period back to the strategy.
|
||||||
elif not self.follow_mode:
|
elif not self.follow_mode:
|
||||||
self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"])
|
self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"])
|
||||||
if self.dk.backtest_live_models:
|
|
||||||
logger.info(
|
|
||||||
f"Backtesting {len(self.dk.backtesting_timeranges)} timeranges (live models)")
|
|
||||||
else:
|
|
||||||
logger.info(f"Training {len(self.dk.training_timeranges)} timeranges")
|
|
||||||
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=metadata["pair"]
|
||||||
)
|
)
|
||||||
|
if not self.config.get("freqai_backtest_live_models", False):
|
||||||
|
logger.info(f"Training {len(self.dk.training_timeranges)} timeranges")
|
||||||
dk = self.start_backtesting(dataframe, metadata, self.dk)
|
dk = self.start_backtesting(dataframe, metadata, self.dk)
|
||||||
|
|
||||||
dataframe = dk.remove_features_from_df(dk.return_dataframe)
|
dataframe = dk.remove_features_from_df(dk.return_dataframe)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
"Backtesting using historic predictions (live models)")
|
||||||
|
dk = self.start_backtesting_from_historic_predictions(
|
||||||
|
dataframe, metadata, self.dk)
|
||||||
|
dataframe = dk.return_dataframe
|
||||||
|
|
||||||
self.clean_up()
|
self.clean_up()
|
||||||
if self.live:
|
if self.live:
|
||||||
self.inference_timer('stop', metadata["pair"])
|
self.inference_timer('stop', metadata["pair"])
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def clean_up(self):
|
def clean_up(self):
|
||||||
@ -164,6 +176,13 @@ class IFreqaiModel(ABC):
|
|||||||
self.model = None
|
self.model = None
|
||||||
self.dk = None
|
self.dk = None
|
||||||
|
|
||||||
|
def _on_stop(self):
|
||||||
|
"""
|
||||||
|
Callback for Subclasses to override to include logic for shutting down resources
|
||||||
|
when SIGINT is sent.
|
||||||
|
"""
|
||||||
|
return
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
"""
|
"""
|
||||||
Cleans up threads on Shutdown, set stop event. Join threads to wait
|
Cleans up threads on Shutdown, set stop event. Join threads to wait
|
||||||
@ -172,6 +191,9 @@ class IFreqaiModel(ABC):
|
|||||||
logger.info("Stopping FreqAI")
|
logger.info("Stopping FreqAI")
|
||||||
self._stop_event.set()
|
self._stop_event.set()
|
||||||
|
|
||||||
|
self.data_provider = None
|
||||||
|
self._on_stop()
|
||||||
|
|
||||||
logger.info("Waiting on Training iteration")
|
logger.info("Waiting on Training iteration")
|
||||||
for _thread in self._threads:
|
for _thread in self._threads:
|
||||||
_thread.join()
|
_thread.join()
|
||||||
@ -301,10 +323,11 @@ class IFreqaiModel(ABC):
|
|||||||
self.model = self.dd.load_data(pair, dk)
|
self.model = self.dd.load_data(pair, dk)
|
||||||
|
|
||||||
pred_df, do_preds = self.predict(dataframe_backtest, dk)
|
pred_df, do_preds = self.predict(dataframe_backtest, dk)
|
||||||
append_df = dk.get_predictions_to_append(pred_df, do_preds)
|
append_df = dk.get_predictions_to_append(pred_df, do_preds, dataframe_backtest)
|
||||||
dk.append_predictions(append_df)
|
dk.append_predictions(append_df)
|
||||||
dk.save_backtesting_prediction(append_df)
|
dk.save_backtesting_prediction(append_df)
|
||||||
|
|
||||||
|
self.backtesting_fit_live_predictions(dk)
|
||||||
dk.fill_predictions(dataframe)
|
dk.fill_predictions(dataframe)
|
||||||
|
|
||||||
return dk
|
return dk
|
||||||
@ -617,6 +640,8 @@ class IFreqaiModel(ABC):
|
|||||||
self.dd.historic_predictions[pair] = pred_df
|
self.dd.historic_predictions[pair] = pred_df
|
||||||
hist_preds_df = self.dd.historic_predictions[pair]
|
hist_preds_df = self.dd.historic_predictions[pair]
|
||||||
|
|
||||||
|
self.set_start_dry_live_date(strat_df)
|
||||||
|
|
||||||
for label in hist_preds_df.columns:
|
for label in hist_preds_df.columns:
|
||||||
if hist_preds_df[label].dtype == object:
|
if hist_preds_df[label].dtype == object:
|
||||||
continue
|
continue
|
||||||
@ -629,7 +654,7 @@ class IFreqaiModel(ABC):
|
|||||||
hist_preds_df['DI_values'] = 0
|
hist_preds_df['DI_values'] = 0
|
||||||
|
|
||||||
for return_str in dk.data['extra_returns_per_train']:
|
for return_str in dk.data['extra_returns_per_train']:
|
||||||
hist_preds_df[return_str] = 0
|
hist_preds_df[return_str] = dk.data['extra_returns_per_train'][return_str]
|
||||||
|
|
||||||
hist_preds_df['close_price'] = strat_df['close']
|
hist_preds_df['close_price'] = strat_df['close']
|
||||||
hist_preds_df['date_pred'] = strat_df['date']
|
hist_preds_df['date_pred'] = strat_df['date']
|
||||||
@ -657,7 +682,8 @@ class IFreqaiModel(ABC):
|
|||||||
for label in full_labels:
|
for label in full_labels:
|
||||||
if self.dd.historic_predictions[dk.pair][label].dtype == object:
|
if self.dd.historic_predictions[dk.pair][label].dtype == object:
|
||||||
continue
|
continue
|
||||||
f = spy.stats.norm.fit(self.dd.historic_predictions[dk.pair][label].tail(num_candles))
|
f = spy.stats.norm.fit(
|
||||||
|
self.dd.historic_predictions[dk.pair][label].tail(num_candles))
|
||||||
dk.data["labels_mean"][label], dk.data["labels_std"][label] = f[0], f[1]
|
dk.data["labels_mean"][label], dk.data["labels_std"][label] = f[0], f[1]
|
||||||
|
|
||||||
return
|
return
|
||||||
@ -788,14 +814,8 @@ class IFreqaiModel(ABC):
|
|||||||
:return: if the data exists or not
|
:return: if the data exists or not
|
||||||
"""
|
"""
|
||||||
if self.config.get("freqai_backtest_live_models", False) and len(dataframe_backtest) == 0:
|
if self.config.get("freqai_backtest_live_models", False) and len(dataframe_backtest) == 0:
|
||||||
tr_backtest_startts_str = datetime.fromtimestamp(
|
logger.info(f"No data found for pair {pair} from "
|
||||||
tr_backtest.startts,
|
f"from { tr_backtest.start_fmt} to {tr_backtest.stop_fmt}. "
|
||||||
tz=timezone.utc).strftime(DATETIME_PRINT_FORMAT)
|
|
||||||
tr_backtest_stopts_str = datetime.fromtimestamp(
|
|
||||||
tr_backtest.stopts,
|
|
||||||
tz=timezone.utc).strftime(DATETIME_PRINT_FORMAT)
|
|
||||||
logger.info(f"No data found for pair {pair} from {tr_backtest_startts_str} "
|
|
||||||
f" from {tr_backtest_startts_str} to {tr_backtest_stopts_str}. "
|
|
||||||
"Probably more than one training within the same candle period.")
|
"Probably more than one training within the same candle period.")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
@ -810,20 +830,88 @@ class IFreqaiModel(ABC):
|
|||||||
:param pair: the current pair
|
:param pair: the current pair
|
||||||
:param total_trains: total trains (total number of slides for the sliding window)
|
:param total_trains: total trains (total number of slides for the sliding window)
|
||||||
"""
|
"""
|
||||||
tr_train_startts_str = datetime.fromtimestamp(
|
|
||||||
tr_train.startts,
|
|
||||||
tz=timezone.utc).strftime(DATETIME_PRINT_FORMAT)
|
|
||||||
tr_train_stopts_str = datetime.fromtimestamp(
|
|
||||||
tr_train.stopts,
|
|
||||||
tz=timezone.utc).strftime(DATETIME_PRINT_FORMAT)
|
|
||||||
|
|
||||||
if not self.config.get("freqai_backtest_live_models", False):
|
if not self.config.get("freqai_backtest_live_models", False):
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Training {pair}, {self.pair_it}/{self.total_pairs} pairs"
|
f"Training {pair}, {self.pair_it}/{self.total_pairs} pairs"
|
||||||
f" from {tr_train_startts_str} "
|
f" from {tr_train.start_fmt} "
|
||||||
f"to {tr_train_stopts_str}, {train_it}/{total_trains} "
|
f"to {tr_train.stop_fmt}, {train_it}/{total_trains} "
|
||||||
"trains"
|
"trains"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def backtesting_fit_live_predictions(self, dk: FreqaiDataKitchen):
|
||||||
|
"""
|
||||||
|
Apply fit_live_predictions function in backtesting with a dummy historic_predictions
|
||||||
|
The loop is required to simulate dry/live operation, as it is not possible to predict
|
||||||
|
the type of logic implemented by the user.
|
||||||
|
:param dk: datakitchen object
|
||||||
|
"""
|
||||||
|
fit_live_predictions_candles = self.freqai_info.get("fit_live_predictions_candles", 0)
|
||||||
|
if fit_live_predictions_candles:
|
||||||
|
logger.info("Applying fit_live_predictions in backtesting")
|
||||||
|
label_columns = [col for col in dk.full_df.columns if (
|
||||||
|
col.startswith("&") and
|
||||||
|
not (col.startswith("&") and col.endswith("_mean")) and
|
||||||
|
not (col.startswith("&") and col.endswith("_std")) and
|
||||||
|
col not in self.dk.data["extra_returns_per_train"])
|
||||||
|
]
|
||||||
|
|
||||||
|
for index in range(len(dk.full_df)):
|
||||||
|
if index >= fit_live_predictions_candles:
|
||||||
|
self.dd.historic_predictions[self.dk.pair] = (
|
||||||
|
dk.full_df.iloc[index - fit_live_predictions_candles:index])
|
||||||
|
self.fit_live_predictions(self.dk, self.dk.pair)
|
||||||
|
for label in label_columns:
|
||||||
|
if dk.full_df[label].dtype == object:
|
||||||
|
continue
|
||||||
|
if "labels_mean" in self.dk.data:
|
||||||
|
dk.full_df.at[index, f"{label}_mean"] = (
|
||||||
|
self.dk.data["labels_mean"][label])
|
||||||
|
if "labels_std" in self.dk.data:
|
||||||
|
dk.full_df.at[index, f"{label}_std"] = self.dk.data["labels_std"][label]
|
||||||
|
|
||||||
|
for extra_col in self.dk.data["extra_returns_per_train"]:
|
||||||
|
dk.full_df.at[index, f"{extra_col}"] = (
|
||||||
|
self.dk.data["extra_returns_per_train"][extra_col])
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def update_metadata(self, metadata: Dict[str, Any]):
|
||||||
|
"""
|
||||||
|
Update global metadata and save the updated json file
|
||||||
|
:param metadata: new global metadata dict
|
||||||
|
"""
|
||||||
|
self.dd.save_global_metadata_to_disk(metadata)
|
||||||
|
self.metadata = metadata
|
||||||
|
|
||||||
|
def set_start_dry_live_date(self, live_dataframe: DataFrame):
|
||||||
|
key_name = "start_dry_live_date"
|
||||||
|
if key_name not in self.metadata:
|
||||||
|
metadata = self.metadata
|
||||||
|
metadata[key_name] = int(
|
||||||
|
pd.to_datetime(live_dataframe.tail(1)["date"].values[0]).timestamp())
|
||||||
|
self.update_metadata(metadata)
|
||||||
|
|
||||||
|
def start_backtesting_from_historic_predictions(
|
||||||
|
self, dataframe: DataFrame, metadata: dict, dk: FreqaiDataKitchen
|
||||||
|
) -> FreqaiDataKitchen:
|
||||||
|
"""
|
||||||
|
:param dataframe: DataFrame = strategy passed dataframe
|
||||||
|
:param metadata: Dict = pair metadata
|
||||||
|
:param dk: FreqaiDataKitchen = Data management/analysis tool associated to present pair only
|
||||||
|
:return:
|
||||||
|
FreqaiDataKitchen = Data management/analysis tool associated to present pair only
|
||||||
|
"""
|
||||||
|
pair = metadata["pair"]
|
||||||
|
dk.return_dataframe = dataframe
|
||||||
|
saved_dataframe = self.dd.historic_predictions[pair]
|
||||||
|
columns_to_drop = list(set(saved_dataframe.columns).intersection(
|
||||||
|
dk.return_dataframe.columns))
|
||||||
|
dk.return_dataframe = dk.return_dataframe.drop(columns=list(columns_to_drop))
|
||||||
|
dk.return_dataframe = pd.merge(
|
||||||
|
dk.return_dataframe, saved_dataframe, how='left', left_on='date', right_on="date_pred")
|
||||||
|
# dk.return_dataframe = dk.return_dataframe[saved_dataframe.columns].fillna(0)
|
||||||
|
return dk
|
||||||
|
|
||||||
# Following methods which are overridden by user made prediction models.
|
# Following methods which are overridden by user made prediction models.
|
||||||
# See freqai/prediction_models/CatboostPredictionModel.py for an example.
|
# See freqai/prediction_models/CatboostPredictionModel.py for an example.
|
||||||
|
|
||||||
|
141
freqtrade/freqai/prediction_models/ReinforcementLearner.py
Normal file
141
freqtrade/freqai/prediction_models/ReinforcementLearner.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
import torch as th
|
||||||
|
|
||||||
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
|
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions
|
||||||
|
from freqtrade.freqai.RL.BaseReinforcementLearningModel import BaseReinforcementLearningModel
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ReinforcementLearner(BaseReinforcementLearningModel):
|
||||||
|
"""
|
||||||
|
Reinforcement Learning Model prediction model.
|
||||||
|
|
||||||
|
Users can inherit from this class to make their own RL model with custom
|
||||||
|
environment/training controls. Define the file as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner
|
||||||
|
|
||||||
|
class MyCoolRLModel(ReinforcementLearner):
|
||||||
|
```
|
||||||
|
|
||||||
|
Save the file to `user_data/freqaimodels`, then run it with:
|
||||||
|
|
||||||
|
freqtrade trade --freqaimodel MyCoolRLModel --config config.json --strategy SomeCoolStrat
|
||||||
|
|
||||||
|
Here the users can override any of the functions
|
||||||
|
available in the `IFreqaiModel` inheritance tree. Most importantly for RL, this
|
||||||
|
is where the user overrides `MyRLEnv` (see below), to define custom
|
||||||
|
`calculate_reward()` function, or to override any other parts of the environment.
|
||||||
|
|
||||||
|
This class also allows users to override any other part of the IFreqaiModel tree.
|
||||||
|
For example, the user can override `def fit()` or `def train()` or `def predict()`
|
||||||
|
to take fine-tuned control over these processes.
|
||||||
|
|
||||||
|
Another common override may be `def data_cleaning_predict()` where the user can
|
||||||
|
take fine-tuned control over the data handling pipeline.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen, **kwargs):
|
||||||
|
"""
|
||||||
|
User customizable fit method
|
||||||
|
:param data_dictionary: dict = common data dictionary containing all train/test
|
||||||
|
features/labels/weights.
|
||||||
|
:param dk: FreqaiDatakitchen = data kitchen for current pair.
|
||||||
|
:return:
|
||||||
|
model Any = trained model to be used for inference in dry/live/backtesting
|
||||||
|
"""
|
||||||
|
train_df = data_dictionary["train_features"]
|
||||||
|
total_timesteps = self.freqai_info["rl_config"]["train_cycles"] * len(train_df)
|
||||||
|
|
||||||
|
policy_kwargs = dict(activation_fn=th.nn.ReLU,
|
||||||
|
net_arch=self.net_arch)
|
||||||
|
|
||||||
|
if dk.pair not in self.dd.model_dictionary or not self.continual_learning:
|
||||||
|
model = self.MODELCLASS(self.policy_type, self.train_env, policy_kwargs=policy_kwargs,
|
||||||
|
tensorboard_log=Path(
|
||||||
|
dk.full_path / "tensorboard" / dk.pair.split('/')[0]),
|
||||||
|
**self.freqai_info['model_training_parameters']
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info('Continual training activated - starting training from previously '
|
||||||
|
'trained agent.')
|
||||||
|
model = self.dd.model_dictionary[dk.pair]
|
||||||
|
model.set_env(self.train_env)
|
||||||
|
|
||||||
|
model.learn(
|
||||||
|
total_timesteps=int(total_timesteps),
|
||||||
|
callback=self.eval_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
if Path(dk.data_path / "best_model.zip").is_file():
|
||||||
|
logger.info('Callback found a best model.')
|
||||||
|
best_model = self.MODELCLASS.load(dk.data_path / "best_model")
|
||||||
|
return best_model
|
||||||
|
|
||||||
|
logger.info('Couldnt find best model, using final model instead.')
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
class MyRLEnv(Base5ActionRLEnv):
|
||||||
|
"""
|
||||||
|
User can override any function in BaseRLEnv and gym.Env. Here the user
|
||||||
|
sets a custom reward based on profit and trade duration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def calculate_reward(self, action: int) -> float:
|
||||||
|
"""
|
||||||
|
An example reward function. This is the one function that users will likely
|
||||||
|
wish to inject their own creativity into.
|
||||||
|
:param action: int = The action made by the agent for the current candle.
|
||||||
|
:return:
|
||||||
|
float = the reward to give to the agent for current step (used for optimization
|
||||||
|
of weights in NN)
|
||||||
|
"""
|
||||||
|
# first, penalize if the action is not valid
|
||||||
|
if not self._is_valid(action):
|
||||||
|
return -2
|
||||||
|
|
||||||
|
pnl = self.get_unrealized_profit()
|
||||||
|
factor = 100.
|
||||||
|
|
||||||
|
# reward agent for entering trades
|
||||||
|
if (action in (Actions.Long_enter.value, Actions.Short_enter.value)
|
||||||
|
and self._position == Positions.Neutral):
|
||||||
|
return 25
|
||||||
|
# discourage agent from not entering trades
|
||||||
|
if action == Actions.Neutral.value and self._position == Positions.Neutral:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
|
||||||
|
trade_duration = self._current_tick - self._last_trade_tick # type: ignore
|
||||||
|
|
||||||
|
if trade_duration <= max_trade_duration:
|
||||||
|
factor *= 1.5
|
||||||
|
elif trade_duration > max_trade_duration:
|
||||||
|
factor *= 0.5
|
||||||
|
|
||||||
|
# discourage sitting in position
|
||||||
|
if (self._position in (Positions.Short, Positions.Long) and
|
||||||
|
action == Actions.Neutral.value):
|
||||||
|
return -1 * trade_duration / max_trade_duration
|
||||||
|
|
||||||
|
# close long
|
||||||
|
if action == Actions.Long_exit.value and self._position == Positions.Long:
|
||||||
|
if pnl > self.profit_aim * self.rr:
|
||||||
|
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||||
|
return float(pnl * factor)
|
||||||
|
|
||||||
|
# close short
|
||||||
|
if action == Actions.Short_exit.value and self._position == Positions.Short:
|
||||||
|
if pnl > self.profit_aim * self.rr:
|
||||||
|
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||||
|
return float(pnl * factor)
|
||||||
|
|
||||||
|
return 0.
|
@ -0,0 +1,51 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Any, Dict # , Tuple
|
||||||
|
|
||||||
|
# import numpy.typing as npt
|
||||||
|
from pandas import DataFrame
|
||||||
|
from stable_baselines3.common.callbacks import EvalCallback
|
||||||
|
from stable_baselines3.common.vec_env import SubprocVecEnv
|
||||||
|
|
||||||
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
|
from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner
|
||||||
|
from freqtrade.freqai.RL.BaseReinforcementLearningModel import make_env
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ReinforcementLearner_multiproc(ReinforcementLearner):
|
||||||
|
"""
|
||||||
|
Demonstration of how to build vectorized environments
|
||||||
|
"""
|
||||||
|
|
||||||
|
def set_train_and_eval_environments(self, data_dictionary: Dict[str, Any],
|
||||||
|
prices_train: DataFrame, prices_test: DataFrame,
|
||||||
|
dk: FreqaiDataKitchen):
|
||||||
|
"""
|
||||||
|
User can override this if they are using a custom MyRLEnv
|
||||||
|
:param data_dictionary: dict = common data dictionary containing train and test
|
||||||
|
features/labels/weights.
|
||||||
|
:param prices_train/test: DataFrame = dataframe comprised of the prices to be used in
|
||||||
|
the environment during training
|
||||||
|
or testing
|
||||||
|
:param dk: FreqaiDataKitchen = the datakitchen for the current pair
|
||||||
|
"""
|
||||||
|
train_df = data_dictionary["train_features"]
|
||||||
|
test_df = data_dictionary["test_features"]
|
||||||
|
|
||||||
|
env_id = "train_env"
|
||||||
|
self.train_env = SubprocVecEnv([make_env(self.MyRLEnv, env_id, i, 1, train_df, prices_train,
|
||||||
|
self.reward_params, self.CONV_WIDTH, monitor=True,
|
||||||
|
config=self.config) for i
|
||||||
|
in range(self.max_threads)])
|
||||||
|
|
||||||
|
eval_env_id = 'eval_env'
|
||||||
|
self.eval_env = SubprocVecEnv([make_env(self.MyRLEnv, eval_env_id, i, 1,
|
||||||
|
test_df, prices_test,
|
||||||
|
self.reward_params, self.CONV_WIDTH, monitor=True,
|
||||||
|
config=self.config) for i
|
||||||
|
in range(self.max_threads)])
|
||||||
|
self.eval_callback = EvalCallback(self.eval_env, deterministic=True,
|
||||||
|
render=False, eval_freq=len(train_df),
|
||||||
|
best_model_save_path=str(dk.data_path))
|
@ -14,6 +14,7 @@ from freqtrade.data.history.history_utils import refresh_backtest_ohlcv_data
|
|||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_seconds
|
||||||
from freqtrade.exchange.exchange import market_is_active
|
from freqtrade.exchange.exchange import market_is_active
|
||||||
|
from freqtrade.freqai.data_drawer import FreqaiDataDrawer
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
||||||
|
|
||||||
@ -229,8 +230,6 @@ def get_timerange_backtest_live_models(config: Config) -> str:
|
|||||||
"""
|
"""
|
||||||
dk = FreqaiDataKitchen(config)
|
dk = FreqaiDataKitchen(config)
|
||||||
models_path = dk.get_full_models_path(config)
|
models_path = dk.get_full_models_path(config)
|
||||||
timerange, _ = dk.get_timerange_and_assets_end_dates_from_ready_models(models_path)
|
dd = FreqaiDataDrawer(models_path, config)
|
||||||
start_date = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
|
timerange = dd.get_timerange_from_live_historic_predictions()
|
||||||
end_date = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
|
return timerange.timerange_str
|
||||||
tr = f"{start_date.strftime('%Y%m%d')}-{end_date.strftime('%Y%m%d')}"
|
|
||||||
return tr
|
|
||||||
|
@ -191,10 +191,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Check whether markets have to be reloaded and reload them when it's needed
|
# Check whether markets have to be reloaded and reload them when it's needed
|
||||||
self.exchange.reload_markets()
|
self.exchange.reload_markets()
|
||||||
|
|
||||||
self.update_closed_trades_without_assigned_fees()
|
self.update_trades_without_assigned_fees()
|
||||||
|
|
||||||
# Query trades from persistence layer
|
# Query trades from persistence layer
|
||||||
trades = Trade.get_open_trades()
|
trades: List[Trade] = Trade.get_open_trades()
|
||||||
|
|
||||||
self.active_pair_whitelist = self._refresh_active_whitelist(trades)
|
self.active_pair_whitelist = self._refresh_active_whitelist(trades)
|
||||||
|
|
||||||
@ -354,7 +354,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
self._schedule.run_pending()
|
self._schedule.run_pending()
|
||||||
|
|
||||||
def update_closed_trades_without_assigned_fees(self) -> None:
|
def update_trades_without_assigned_fees(self) -> None:
|
||||||
"""
|
"""
|
||||||
Update closed trades without close fees assigned.
|
Update closed trades without close fees assigned.
|
||||||
Only acts when Orders are in the database, otherwise the last order-id is unknown.
|
Only acts when Orders are in the database, otherwise the last order-id is unknown.
|
||||||
@ -381,6 +381,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
trades = Trade.get_open_trades_without_assigned_fees()
|
trades = Trade.get_open_trades_without_assigned_fees()
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
|
with self._exit_lock:
|
||||||
if trade.is_open and not trade.fee_updated(trade.entry_side):
|
if trade.is_open and not trade.fee_updated(trade.entry_side):
|
||||||
order = trade.select_order(trade.entry_side, False)
|
order = trade.select_order(trade.entry_side, False)
|
||||||
open_order = trade.select_order(trade.entry_side, True)
|
open_order = trade.select_order(trade.entry_side, True)
|
||||||
@ -826,6 +827,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
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)
|
trade.update_order(co)
|
||||||
|
# Reset stoploss order id.
|
||||||
|
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}")
|
||||||
return trade
|
return trade
|
||||||
@ -982,7 +985,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# SELL / exit positions / close trades logic and methods
|
# SELL / exit positions / close trades logic and methods
|
||||||
#
|
#
|
||||||
|
|
||||||
def exit_positions(self, trades: List[Any]) -> int:
|
def exit_positions(self, trades: List[Trade]) -> int:
|
||||||
"""
|
"""
|
||||||
Tries to execute exit orders for open trades (positions)
|
Tries to execute exit orders for open trades (positions)
|
||||||
"""
|
"""
|
||||||
@ -1010,7 +1013,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
def handle_trade(self, trade: Trade) -> bool:
|
def handle_trade(self, trade: Trade) -> bool:
|
||||||
"""
|
"""
|
||||||
Sells/exits_short the current pair if the threshold is reached and updates the trade record.
|
Exits the current pair if the threshold is reached and updates the trade record.
|
||||||
:return: True if trade has been sold/exited_short, False otherwise
|
:return: True if trade has been sold/exited_short, False otherwise
|
||||||
"""
|
"""
|
||||||
if not trade.is_open:
|
if not trade.is_open:
|
||||||
@ -1133,10 +1136,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
|
trade.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
|
||||||
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
|
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
|
||||||
stoploss_order=True)
|
stoploss_order=True)
|
||||||
# Lock pair for one candle to prevent immediate rebuys
|
|
||||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
|
||||||
reason='Auto lock')
|
|
||||||
self._notify_exit(trade, "stoploss", True)
|
self._notify_exit(trade, "stoploss", True)
|
||||||
|
self.handle_protections(trade.pair, trade.trade_direction)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if trade.open_order_id or not trade.is_open:
|
if trade.open_order_id or not trade.is_open:
|
||||||
@ -1150,7 +1151,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
stoploss = (
|
stoploss = (
|
||||||
self.edge.stoploss(pair=trade.pair)
|
self.edge.stoploss(pair=trade.pair)
|
||||||
if self.edge else
|
if self.edge else
|
||||||
self.strategy.stoploss / trade.leverage
|
trade.stop_loss_pct / trade.leverage
|
||||||
)
|
)
|
||||||
if trade.is_short:
|
if trade.is_short:
|
||||||
stop_price = trade.open_rate * (1 - stoploss)
|
stop_price = trade.open_rate * (1 - stoploss)
|
||||||
@ -1169,7 +1170,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation):
|
if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation):
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
trade.stoploss_order_id = None
|
|
||||||
logger.warning('Stoploss order was cancelled, but unable to recreate one.')
|
logger.warning('Stoploss order was cancelled, but unable to recreate one.')
|
||||||
|
|
||||||
# Finally we check if stoploss on exchange should be moved up because of trailing.
|
# Finally we check if stoploss on exchange should be moved up because of trailing.
|
||||||
@ -1595,11 +1595,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade.close_rate_requested = limit
|
trade.close_rate_requested = limit
|
||||||
trade.exit_reason = exit_reason
|
trade.exit_reason = exit_reason
|
||||||
|
|
||||||
if not sub_trade_amt:
|
|
||||||
# Lock pair for one candle to prevent immediate re-trading
|
|
||||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
|
||||||
reason='Auto lock')
|
|
||||||
|
|
||||||
self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj)
|
self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj)
|
||||||
# In case of market sell orders the order can be closed immediately
|
# In case of market sell orders the order can be closed immediately
|
||||||
if order.get('status', 'unknown') in ('closed', 'expired'):
|
if order.get('status', 'unknown') in ('closed', 'expired'):
|
||||||
@ -1809,6 +1804,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self._notify_enter(trade, order, fill=True, sub_trade=sub_trade)
|
self._notify_enter(trade, order, 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
|
||||||
|
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 = {'type': RPCMessageType.PROTECTION_TRIGGER, }
|
||||||
|
@ -10,7 +10,8 @@ from typing import Any, Dict, Iterator, List, Mapping, Union
|
|||||||
from typing.io import IO
|
from typing.io import IO
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import pandas
|
import orjson
|
||||||
|
import pandas as pd
|
||||||
import rapidjson
|
import rapidjson
|
||||||
|
|
||||||
from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN
|
from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN
|
||||||
@ -256,29 +257,37 @@ def parse_db_uri_for_logging(uri: str):
|
|||||||
return parsed_db_uri.geturl().replace(f':{pwd}@', ':*****@')
|
return parsed_db_uri.geturl().replace(f':{pwd}@', ':*****@')
|
||||||
|
|
||||||
|
|
||||||
def dataframe_to_json(dataframe: pandas.DataFrame) -> str:
|
def dataframe_to_json(dataframe: pd.DataFrame) -> str:
|
||||||
"""
|
"""
|
||||||
Serialize a DataFrame for transmission over the wire using JSON
|
Serialize a DataFrame for transmission over the wire using JSON
|
||||||
:param dataframe: A pandas DataFrame
|
:param dataframe: A pandas DataFrame
|
||||||
:returns: A JSON string of the pandas DataFrame
|
:returns: A JSON string of the pandas DataFrame
|
||||||
"""
|
"""
|
||||||
return dataframe.to_json(orient='split')
|
# https://github.com/pandas-dev/pandas/issues/24889
|
||||||
|
# https://github.com/pandas-dev/pandas/issues/40443
|
||||||
|
# We need to convert to a dict to avoid mem leak
|
||||||
|
def default(z):
|
||||||
|
if isinstance(z, pd.Timestamp):
|
||||||
|
return z.timestamp() * 1e3
|
||||||
|
raise TypeError
|
||||||
|
|
||||||
|
return str(orjson.dumps(dataframe.to_dict(orient='split'), default=default), 'utf-8')
|
||||||
|
|
||||||
|
|
||||||
def json_to_dataframe(data: str) -> pandas.DataFrame:
|
def json_to_dataframe(data: str) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Deserialize JSON into a DataFrame
|
Deserialize JSON into a DataFrame
|
||||||
:param data: A JSON string
|
:param data: A JSON string
|
||||||
:returns: A pandas DataFrame from the JSON string
|
:returns: A pandas DataFrame from the JSON string
|
||||||
"""
|
"""
|
||||||
dataframe = pandas.read_json(data, orient='split')
|
dataframe = pd.read_json(data, orient='split')
|
||||||
if 'date' in dataframe.columns:
|
if 'date' in dataframe.columns:
|
||||||
dataframe['date'] = pandas.to_datetime(dataframe['date'], unit='ms', utc=True)
|
dataframe['date'] = pd.to_datetime(dataframe['date'], unit='ms', utc=True)
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
def remove_entry_exit_signals(dataframe: pandas.DataFrame):
|
def remove_entry_exit_signals(dataframe: pd.DataFrame):
|
||||||
"""
|
"""
|
||||||
Remove Entry and Exit signals from a DataFrame
|
Remove Entry and Exit signals from a DataFrame
|
||||||
|
|
||||||
|
@ -692,10 +692,11 @@ class Backtesting:
|
|||||||
trade.orders.append(order)
|
trade.orders.append(order)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
def _get_exit_trade_entry(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]:
|
def _get_exit_trade_entry(
|
||||||
|
self, trade: LocalTrade, row: Tuple, is_first: bool) -> Optional[LocalTrade]:
|
||||||
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||||
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if is_first and self.trading_mode == TradingMode.FUTURES:
|
||||||
trade.funding_fees = self.exchange.calculate_funding_fees(
|
trade.funding_fees = self.exchange.calculate_funding_fees(
|
||||||
self.futures_data[trade.pair],
|
self.futures_data[trade.pair],
|
||||||
amount=trade.amount,
|
amount=trade.amount,
|
||||||
@ -704,31 +705,6 @@ class Backtesting:
|
|||||||
close_date=exit_candle_time,
|
close_date=exit_candle_time,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.timeframe_detail and trade.pair in self.detail_data:
|
|
||||||
exit_candle_end = exit_candle_time + timedelta(minutes=self.timeframe_min)
|
|
||||||
|
|
||||||
detail_data = self.detail_data[trade.pair]
|
|
||||||
detail_data = detail_data.loc[
|
|
||||||
(detail_data['date'] >= exit_candle_time) &
|
|
||||||
(detail_data['date'] < exit_candle_end)
|
|
||||||
].copy()
|
|
||||||
if len(detail_data) == 0:
|
|
||||||
# Fall back to "regular" data if no detail data was found for this candle
|
|
||||||
return self._get_exit_trade_entry_for_candle(trade, row)
|
|
||||||
detail_data.loc[:, 'enter_long'] = row[LONG_IDX]
|
|
||||||
detail_data.loc[:, 'exit_long'] = row[ELONG_IDX]
|
|
||||||
detail_data.loc[:, 'enter_short'] = row[SHORT_IDX]
|
|
||||||
detail_data.loc[:, 'exit_short'] = row[ESHORT_IDX]
|
|
||||||
detail_data.loc[:, 'enter_tag'] = row[ENTER_TAG_IDX]
|
|
||||||
detail_data.loc[:, 'exit_tag'] = row[EXIT_TAG_IDX]
|
|
||||||
for det_row in detail_data[HEADERS].values.tolist():
|
|
||||||
res = self._get_exit_trade_entry_for_candle(trade, det_row)
|
|
||||||
if res:
|
|
||||||
return res
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
else:
|
|
||||||
return self._get_exit_trade_entry_for_candle(trade, row)
|
return self._get_exit_trade_entry_for_candle(trade, row)
|
||||||
|
|
||||||
def get_valid_price_and_stake(
|
def get_valid_price_and_stake(
|
||||||
@ -1074,7 +1050,7 @@ class Backtesting:
|
|||||||
|
|
||||||
def backtest_loop(
|
def backtest_loop(
|
||||||
self, row: Tuple, pair: str, current_time: datetime, end_date: datetime,
|
self, row: Tuple, pair: str, current_time: datetime, end_date: datetime,
|
||||||
max_open_trades: int, open_trade_count_start: int) -> int:
|
max_open_trades: int, open_trade_count_start: int, is_first: bool = True) -> int:
|
||||||
"""
|
"""
|
||||||
NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
|
NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
|
||||||
|
|
||||||
@ -1092,9 +1068,11 @@ class Backtesting:
|
|||||||
# without positionstacking, we can only have one open trade per pair.
|
# without positionstacking, we can only have one open trade per pair.
|
||||||
# max_open_trades must be respected
|
# max_open_trades must be respected
|
||||||
# don't open on the last row
|
# don't open on the last row
|
||||||
|
# We only open trades on the main candle, not on detail candles
|
||||||
trade_dir = self.check_for_trade_entry(row)
|
trade_dir = self.check_for_trade_entry(row)
|
||||||
if (
|
if (
|
||||||
(self._position_stacking or len(LocalTrade.bt_trades_open_pp[pair]) == 0)
|
(self._position_stacking or len(LocalTrade.bt_trades_open_pp[pair]) == 0)
|
||||||
|
and is_first
|
||||||
and self.trade_slot_available(max_open_trades, open_trade_count_start)
|
and self.trade_slot_available(max_open_trades, open_trade_count_start)
|
||||||
and current_time != end_date
|
and current_time != end_date
|
||||||
and trade_dir is not None
|
and trade_dir is not None
|
||||||
@ -1120,7 +1098,7 @@ class Backtesting:
|
|||||||
|
|
||||||
# 4. Create exit orders (if any)
|
# 4. Create exit orders (if any)
|
||||||
if not trade.open_order_id:
|
if not trade.open_order_id:
|
||||||
self._get_exit_trade_entry(trade, row) # Place exit order if necessary
|
self._get_exit_trade_entry(trade, row, is_first) # Place exit order if necessary
|
||||||
|
|
||||||
# 5. Process exit orders.
|
# 5. Process exit orders.
|
||||||
order = trade.select_order(trade.exit_side, is_open=True)
|
order = trade.select_order(trade.exit_side, is_open=True)
|
||||||
@ -1171,7 +1149,6 @@ class Backtesting:
|
|||||||
|
|
||||||
self.progress.init_step(BacktestState.BACKTEST, int(
|
self.progress.init_step(BacktestState.BACKTEST, int(
|
||||||
(end_date - start_date) / timedelta(minutes=self.timeframe_min)))
|
(end_date - start_date) / timedelta(minutes=self.timeframe_min)))
|
||||||
|
|
||||||
# Loop timerange and get candle for each pair at that point in time
|
# Loop timerange and get candle for each pair at that point in time
|
||||||
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
|
||||||
@ -1185,7 +1162,35 @@ class Backtesting:
|
|||||||
row_index += 1
|
row_index += 1
|
||||||
indexes[pair] = row_index
|
indexes[pair] = row_index
|
||||||
self.dataprovider._set_dataframe_max_index(row_index)
|
self.dataprovider._set_dataframe_max_index(row_index)
|
||||||
|
current_detail_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||||
|
if self.timeframe_detail and pair in self.detail_data:
|
||||||
|
exit_candle_end = current_detail_time + timedelta(minutes=self.timeframe_min)
|
||||||
|
|
||||||
|
detail_data = self.detail_data[pair]
|
||||||
|
detail_data = detail_data.loc[
|
||||||
|
(detail_data['date'] >= current_detail_time) &
|
||||||
|
(detail_data['date'] < exit_candle_end)
|
||||||
|
].copy()
|
||||||
|
if len(detail_data) == 0:
|
||||||
|
# Fall back to "regular" data if no detail data was found for this candle
|
||||||
|
open_trade_count_start = self.backtest_loop(
|
||||||
|
row, pair, current_time, end_date, max_open_trades,
|
||||||
|
open_trade_count_start)
|
||||||
|
detail_data.loc[:, 'enter_long'] = row[LONG_IDX]
|
||||||
|
detail_data.loc[:, 'exit_long'] = row[ELONG_IDX]
|
||||||
|
detail_data.loc[:, 'enter_short'] = row[SHORT_IDX]
|
||||||
|
detail_data.loc[:, 'exit_short'] = row[ESHORT_IDX]
|
||||||
|
detail_data.loc[:, 'enter_tag'] = row[ENTER_TAG_IDX]
|
||||||
|
detail_data.loc[:, 'exit_tag'] = row[EXIT_TAG_IDX]
|
||||||
|
is_first = True
|
||||||
|
current_time_det = current_time
|
||||||
|
for det_row in detail_data[HEADERS].values.tolist():
|
||||||
|
open_trade_count_start = self.backtest_loop(
|
||||||
|
det_row, pair, current_time_det, end_date, max_open_trades,
|
||||||
|
open_trade_count_start, is_first)
|
||||||
|
current_time_det += timedelta(minutes=self.timeframe_detail_min)
|
||||||
|
is_first = False
|
||||||
|
else:
|
||||||
open_trade_count_start = self.backtest_loop(
|
open_trade_count_start = self.backtest_loop(
|
||||||
row, pair, current_time, end_date, max_open_trades, open_trade_count_start)
|
row, pair, current_time, end_date, max_open_trades, open_trade_count_start)
|
||||||
|
|
||||||
@ -1286,8 +1291,7 @@ class Backtesting:
|
|||||||
def _get_min_cached_backtest_date(self):
|
def _get_min_cached_backtest_date(self):
|
||||||
min_backtest_date = None
|
min_backtest_date = None
|
||||||
backtest_cache_age = self.config.get('backtest_cache', constants.BACKTEST_CACHE_DEFAULT)
|
backtest_cache_age = self.config.get('backtest_cache', constants.BACKTEST_CACHE_DEFAULT)
|
||||||
if self.timerange.stopts == 0 or datetime.fromtimestamp(
|
if self.timerange.stopts == 0 or self.timerange.stopdt > datetime.now(tz=timezone.utc):
|
||||||
self.timerange.stopts, tz=timezone.utc) > datetime.now(tz=timezone.utc):
|
|
||||||
logger.warning('Backtest result caching disabled due to use of open-ended timerange.')
|
logger.warning('Backtest result caching disabled due to use of open-ended timerange.')
|
||||||
elif backtest_cache_age == 'day':
|
elif backtest_cache_age == 'day':
|
||||||
min_backtest_date = datetime.now(tz=timezone.utc) - timedelta(days=1)
|
min_backtest_date = datetime.now(tz=timezone.utc) - timedelta(days=1)
|
||||||
|
@ -17,6 +17,7 @@ from freqtrade.enums import HyperoptState
|
|||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import deep_merge_dicts, round_coin_value, round_dict, safe_value_fallback2
|
from freqtrade.misc import deep_merge_dicts, round_coin_value, round_dict, safe_value_fallback2
|
||||||
from freqtrade.optimize.hyperopt_epoch_filters import hyperopt_filter_epochs
|
from freqtrade.optimize.hyperopt_epoch_filters import hyperopt_filter_epochs
|
||||||
|
from freqtrade.optimize.optimize_reports import generate_wins_draws_losses
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -325,8 +326,10 @@ class HyperoptTools():
|
|||||||
|
|
||||||
# New mode, using backtest result for metrics
|
# New mode, using backtest result for metrics
|
||||||
trials['results_metrics.winsdrawslosses'] = trials.apply(
|
trials['results_metrics.winsdrawslosses'] = trials.apply(
|
||||||
lambda x: f"{x['results_metrics.wins']} {x['results_metrics.draws']:>4} "
|
lambda x: generate_wins_draws_losses(
|
||||||
f"{x['results_metrics.losses']:>4}", axis=1)
|
x['results_metrics.wins'], x['results_metrics.draws'],
|
||||||
|
x['results_metrics.losses']
|
||||||
|
), axis=1)
|
||||||
|
|
||||||
trials = trials[['Best', 'current_epoch', 'results_metrics.total_trades',
|
trials = trials[['Best', 'current_epoch', 'results_metrics.total_trades',
|
||||||
'results_metrics.winsdrawslosses',
|
'results_metrics.winsdrawslosses',
|
||||||
@ -337,7 +340,7 @@ class HyperoptTools():
|
|||||||
'loss', 'is_initial_point', 'is_random', 'is_best']]
|
'loss', 'is_initial_point', 'is_random', 'is_best']]
|
||||||
|
|
||||||
trials.columns = [
|
trials.columns = [
|
||||||
'Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit',
|
'Best', 'Epoch', 'Trades', ' Win Draw Loss Win%', 'Avg profit',
|
||||||
'Total profit', 'Profit', 'Avg duration', 'max_drawdown', 'max_drawdown_account',
|
'Total profit', 'Profit', 'Avg duration', 'max_drawdown', 'max_drawdown_account',
|
||||||
'max_drawdown_abs', 'Objective', 'is_initial_point', 'is_random', 'is_best'
|
'max_drawdown_abs', 'Objective', 'is_initial_point', 'is_random', 'is_best'
|
||||||
]
|
]
|
||||||
@ -467,9 +470,9 @@ class HyperoptTools():
|
|||||||
|
|
||||||
base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades',
|
base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades',
|
||||||
'results_metrics.profit_mean', 'results_metrics.profit_median',
|
'results_metrics.profit_mean', 'results_metrics.profit_median',
|
||||||
'results_metrics.profit_total',
|
'results_metrics.profit_total', 'Stake currency',
|
||||||
'Stake currency',
|
|
||||||
'results_metrics.profit_total_abs', 'results_metrics.holding_avg',
|
'results_metrics.profit_total_abs', 'results_metrics.holding_avg',
|
||||||
|
'results_metrics.trade_count_long', 'results_metrics.trade_count_short',
|
||||||
'loss', 'is_initial_point', 'is_best']
|
'loss', 'is_initial_point', 'is_best']
|
||||||
perc_multi = 100
|
perc_multi = 100
|
||||||
|
|
||||||
@ -477,7 +480,9 @@ class HyperoptTools():
|
|||||||
trials = trials[base_metrics + param_metrics]
|
trials = trials[base_metrics + param_metrics]
|
||||||
|
|
||||||
base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit',
|
base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit',
|
||||||
'Stake currency', 'Profit', 'Avg duration', 'Objective',
|
'Stake currency', 'Profit', 'Avg duration',
|
||||||
|
'Trade count long', 'Trade count short',
|
||||||
|
'Objective',
|
||||||
'is_initial_point', 'is_best']
|
'is_initial_point', 'is_best']
|
||||||
param_columns = list(results[0]['params_dict'].keys())
|
param_columns = list(results[0]['params_dict'].keys())
|
||||||
trials.columns = base_columns + param_columns
|
trials.columns = base_columns + param_columns
|
||||||
|
@ -86,7 +86,7 @@ def _get_line_header(first_column: str, stake_currency: str,
|
|||||||
'Win Draw Loss Win%']
|
'Win Draw Loss Win%']
|
||||||
|
|
||||||
|
|
||||||
def _generate_wins_draws_losses(wins, draws, losses):
|
def generate_wins_draws_losses(wins, draws, losses):
|
||||||
if wins > 0 and losses == 0:
|
if wins > 0 and losses == 0:
|
||||||
wl_ratio = '100'
|
wl_ratio = '100'
|
||||||
elif wins == 0:
|
elif wins == 0:
|
||||||
@ -600,7 +600,7 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st
|
|||||||
output = [[
|
output = [[
|
||||||
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
|
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
|
||||||
t['profit_total_pct'], t['duration_avg'],
|
t['profit_total_pct'], t['duration_avg'],
|
||||||
_generate_wins_draws_losses(t['wins'], t['draws'], t['losses'])
|
generate_wins_draws_losses(t['wins'], t['draws'], t['losses'])
|
||||||
] for t in pair_results]
|
] for t in pair_results]
|
||||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||||
return tabulate(output, headers=headers,
|
return tabulate(output, headers=headers,
|
||||||
@ -626,7 +626,7 @@ def text_table_exit_reason(exit_reason_stats: List[Dict[str, Any]], stake_curren
|
|||||||
|
|
||||||
output = [[
|
output = [[
|
||||||
t.get('exit_reason', t.get('sell_reason')), t['trades'],
|
t.get('exit_reason', t.get('sell_reason')), t['trades'],
|
||||||
_generate_wins_draws_losses(t['wins'], t['draws'], t['losses']),
|
generate_wins_draws_losses(t['wins'], t['draws'], t['losses']),
|
||||||
t['profit_mean_pct'], t['profit_sum_pct'],
|
t['profit_mean_pct'], t['profit_sum_pct'],
|
||||||
round_coin_value(t['profit_total_abs'], stake_currency, False),
|
round_coin_value(t['profit_total_abs'], stake_currency, False),
|
||||||
t['profit_total_pct'],
|
t['profit_total_pct'],
|
||||||
@ -656,7 +656,7 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr
|
|||||||
t['profit_total_abs'],
|
t['profit_total_abs'],
|
||||||
t['profit_total_pct'],
|
t['profit_total_pct'],
|
||||||
t['duration_avg'],
|
t['duration_avg'],
|
||||||
_generate_wins_draws_losses(
|
generate_wins_draws_losses(
|
||||||
t['wins'],
|
t['wins'],
|
||||||
t['draws'],
|
t['draws'],
|
||||||
t['losses'])] for t in tag_results]
|
t['losses'])] for t in tag_results]
|
||||||
@ -715,7 +715,7 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str:
|
|||||||
output = [[
|
output = [[
|
||||||
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
|
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
|
||||||
t['profit_total_pct'], t['duration_avg'],
|
t['profit_total_pct'], t['duration_avg'],
|
||||||
_generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), drawdown]
|
generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), drawdown]
|
||||||
for t, drawdown in zip(strategy_results, drawdown)]
|
for t, drawdown in zip(strategy_results, drawdown)]
|
||||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||||
return tabulate(output, headers=headers,
|
return tabulate(output, headers=headers,
|
||||||
|
@ -87,7 +87,7 @@ class PairLocks():
|
|||||||
Get the lock that expires the latest for the pair given.
|
Get the lock that expires the latest for the pair given.
|
||||||
"""
|
"""
|
||||||
locks = PairLocks.get_pair_locks(pair, now, side=side)
|
locks = PairLocks.get_pair_locks(pair, now, side=side)
|
||||||
locks = sorted(locks, key=lambda l: l.lock_end_time, reverse=True)
|
locks = sorted(locks, key=lambda lock: lock.lock_end_time, reverse=True)
|
||||||
return locks[0] if locks else None
|
return locks[0] if locks else None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -90,6 +90,13 @@ class Order(_DECL_BASE):
|
|||||||
def safe_filled(self) -> float:
|
def safe_filled(self) -> float:
|
||||||
return self.filled if self.filled is not None else self.amount or 0.0
|
return self.filled if self.filled is not None else self.amount or 0.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def safe_remaining(self) -> float:
|
||||||
|
return (
|
||||||
|
self.remaining if self.remaining is not None else
|
||||||
|
self.amount - (self.filled or 0.0)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def safe_fee_base(self) -> float:
|
def safe_fee_base(self) -> float:
|
||||||
return self.ft_fee_base or 0.0
|
return self.ft_fee_base or 0.0
|
||||||
|
@ -81,8 +81,6 @@ async def validate_ws_token(
|
|||||||
except HTTPException:
|
except HTTPException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# No checks passed, deny the connection
|
|
||||||
logger.debug("Denying websocket request.")
|
|
||||||
# If it doesn't match, close the websocket connection
|
# If it doesn't match, close the websocket connection
|
||||||
await ws.close(code=status.WS_1008_POLICY_VIOLATION)
|
await ws.close(code=status.WS_1008_POLICY_VIOLATION)
|
||||||
|
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, WebSocketDisconnect
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi.websockets import WebSocket, WebSocketState
|
from fastapi.websockets import WebSocket
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
from websockets.exceptions import WebSocketException
|
|
||||||
|
|
||||||
from freqtrade.enums import RPCMessageType, RPCRequestType
|
from freqtrade.enums import RPCMessageType, RPCRequestType
|
||||||
from freqtrade.rpc.api_server.api_auth import validate_ws_token
|
from freqtrade.rpc.api_server.api_auth import validate_ws_token
|
||||||
from freqtrade.rpc.api_server.deps import get_channel_manager, get_rpc
|
from freqtrade.rpc.api_server.deps import get_message_stream, get_rpc
|
||||||
from freqtrade.rpc.api_server.ws import WebSocketChannel
|
from freqtrade.rpc.api_server.ws.channel import WebSocketChannel, create_channel
|
||||||
from freqtrade.rpc.api_server.ws.channel import ChannelManager
|
from freqtrade.rpc.api_server.ws.message_stream import MessageStream
|
||||||
from freqtrade.rpc.api_server.ws_schemas import (WSAnalyzedDFMessage, WSMessageSchema,
|
from freqtrade.rpc.api_server.ws_schemas import (WSAnalyzedDFMessage, WSMessageSchema,
|
||||||
WSRequestSchema, WSWhitelistMessage)
|
WSRequestSchema, WSWhitelistMessage)
|
||||||
from freqtrade.rpc.rpc import RPC
|
from freqtrade.rpc.rpc import RPC
|
||||||
@ -22,23 +22,35 @@ logger = logging.getLogger(__name__)
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
async def is_websocket_alive(ws: WebSocket) -> bool:
|
async def channel_reader(channel: WebSocketChannel, rpc: RPC):
|
||||||
"""
|
"""
|
||||||
Check if a FastAPI Websocket is still open
|
Iterate over the messages from the channel and process the request
|
||||||
"""
|
"""
|
||||||
if (
|
async for message in channel:
|
||||||
ws.application_state == WebSocketState.CONNECTED and
|
await _process_consumer_request(message, channel, rpc)
|
||||||
ws.client_state == WebSocketState.CONNECTED
|
|
||||||
):
|
|
||||||
return True
|
async def channel_broadcaster(channel: WebSocketChannel, message_stream: MessageStream):
|
||||||
return False
|
"""
|
||||||
|
Iterate over messages in the message stream and send them
|
||||||
|
"""
|
||||||
|
async for message, ts in message_stream:
|
||||||
|
if channel.subscribed_to(message.get('type')):
|
||||||
|
# Log a warning if this channel is behind
|
||||||
|
# on the message stream by a lot
|
||||||
|
if (time.time() - ts) > 60:
|
||||||
|
logger.warning(f"Channel {channel} is behind MessageStream by 1 minute,"
|
||||||
|
" this can cause a memory leak if you see this message"
|
||||||
|
" often, consider reducing pair list size or amount of"
|
||||||
|
" consumers.")
|
||||||
|
|
||||||
|
await channel.send(message, timeout=True)
|
||||||
|
|
||||||
|
|
||||||
async def _process_consumer_request(
|
async def _process_consumer_request(
|
||||||
request: Dict[str, Any],
|
request: Dict[str, Any],
|
||||||
channel: WebSocketChannel,
|
channel: WebSocketChannel,
|
||||||
rpc: RPC,
|
rpc: RPC
|
||||||
channel_manager: ChannelManager
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Validate and handle a request from a websocket consumer
|
Validate and handle a request from a websocket consumer
|
||||||
@ -74,65 +86,29 @@ async def _process_consumer_request(
|
|||||||
|
|
||||||
# Format response
|
# Format response
|
||||||
response = WSWhitelistMessage(data=whitelist)
|
response = WSWhitelistMessage(data=whitelist)
|
||||||
# Send it back
|
await channel.send(response.dict(exclude_none=True))
|
||||||
await channel_manager.send_direct(channel, response.dict(exclude_none=True))
|
|
||||||
|
|
||||||
elif type == RPCRequestType.ANALYZED_DF:
|
elif type == RPCRequestType.ANALYZED_DF:
|
||||||
limit = None
|
|
||||||
|
|
||||||
if data:
|
|
||||||
# Limit the amount of candles per dataframe to 'limit' or 1500
|
# Limit the amount of candles per dataframe to 'limit' or 1500
|
||||||
limit = max(data.get('limit', 1500), 1500)
|
limit = min(data.get('limit', 1500), 1500) if data else None
|
||||||
|
|
||||||
# For every pair in the generator, send a separate message
|
# For every pair in the generator, send a separate message
|
||||||
for message in rpc._ws_request_analyzed_df(limit):
|
for message in rpc._ws_request_analyzed_df(limit):
|
||||||
|
# Format response
|
||||||
response = WSAnalyzedDFMessage(data=message)
|
response = WSAnalyzedDFMessage(data=message)
|
||||||
await channel_manager.send_direct(channel, response.dict(exclude_none=True))
|
await channel.send(response.dict(exclude_none=True))
|
||||||
|
|
||||||
|
|
||||||
@router.websocket("/message/ws")
|
@router.websocket("/message/ws")
|
||||||
async def message_endpoint(
|
async def message_endpoint(
|
||||||
ws: WebSocket,
|
websocket: WebSocket,
|
||||||
|
token: str = Depends(validate_ws_token),
|
||||||
rpc: RPC = Depends(get_rpc),
|
rpc: RPC = Depends(get_rpc),
|
||||||
channel_manager=Depends(get_channel_manager),
|
message_stream: MessageStream = Depends(get_message_stream)
|
||||||
token: str = Depends(validate_ws_token)
|
|
||||||
):
|
):
|
||||||
"""
|
if token:
|
||||||
Message WebSocket endpoint, facilitates sending RPC messages
|
async with create_channel(websocket) as channel:
|
||||||
"""
|
await channel.run_channel_tasks(
|
||||||
try:
|
channel_reader(channel, rpc),
|
||||||
channel = await channel_manager.on_connect(ws)
|
channel_broadcaster(channel, message_stream)
|
||||||
if await is_websocket_alive(ws):
|
)
|
||||||
|
|
||||||
logger.info(f"Consumer connected - {channel}")
|
|
||||||
|
|
||||||
# Keep connection open until explicitly closed, and process requests
|
|
||||||
try:
|
|
||||||
while not channel.is_closed():
|
|
||||||
request = await channel.recv()
|
|
||||||
|
|
||||||
# Process the request here
|
|
||||||
await _process_consumer_request(request, channel, rpc, channel_manager)
|
|
||||||
|
|
||||||
except (WebSocketDisconnect, WebSocketException):
|
|
||||||
# Handle client disconnects
|
|
||||||
logger.info(f"Consumer disconnected - {channel}")
|
|
||||||
except RuntimeError:
|
|
||||||
# Handle cases like -
|
|
||||||
# RuntimeError('Cannot call "send" once a closed message has been sent')
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
logger.info(f"Consumer connection failed - {channel}: {e}")
|
|
||||||
logger.debug(e, exc_info=e)
|
|
||||||
|
|
||||||
except RuntimeError:
|
|
||||||
# WebSocket was closed
|
|
||||||
# Do nothing
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to serve - {ws.client}")
|
|
||||||
# Log tracebacks to keep track of what errors are happening
|
|
||||||
logger.exception(e)
|
|
||||||
finally:
|
|
||||||
if channel:
|
|
||||||
await channel_manager.on_disconnect(ws)
|
|
||||||
|
@ -41,8 +41,8 @@ def get_exchange(config=Depends(get_config)):
|
|||||||
return ApiServer._exchange
|
return ApiServer._exchange
|
||||||
|
|
||||||
|
|
||||||
def get_channel_manager():
|
def get_message_stream():
|
||||||
return ApiServer._ws_channel_manager
|
return ApiServer._message_stream
|
||||||
|
|
||||||
|
|
||||||
def is_webserver_mode(config=Depends(get_config)):
|
def is_webserver_mode(config=Depends(get_config)):
|
||||||
|
@ -1,22 +1,17 @@
|
|||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
from threading import Thread
|
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import Depends, FastAPI
|
from fastapi import Depends, FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
# Look into alternatives
|
|
||||||
from janus import Queue as ThreadedQueue
|
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.exceptions import OperationalException
|
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 import ChannelManager
|
from freqtrade.rpc.api_server.ws.message_stream import MessageStream
|
||||||
from freqtrade.rpc.api_server.ws_schemas import WSMessageSchemaType
|
|
||||||
from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler
|
from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler
|
||||||
|
|
||||||
|
|
||||||
@ -50,10 +45,8 @@ class ApiServer(RPCHandler):
|
|||||||
_config: Config = {}
|
_config: Config = {}
|
||||||
# Exchange - only available in webserver mode.
|
# Exchange - only available in webserver mode.
|
||||||
_exchange = None
|
_exchange = None
|
||||||
# websocket message queue stuff
|
# websocket message stuff
|
||||||
_ws_channel_manager: ChannelManager
|
_message_stream: Optional[MessageStream] = None
|
||||||
_ws_thread = None
|
|
||||||
_ws_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -71,15 +64,11 @@ class ApiServer(RPCHandler):
|
|||||||
return
|
return
|
||||||
self._standalone: bool = standalone
|
self._standalone: bool = standalone
|
||||||
self._server = None
|
self._server = None
|
||||||
self._ws_queue: Optional[ThreadedQueue] = None
|
|
||||||
self._ws_background_task = None
|
|
||||||
|
|
||||||
ApiServer.__initialized = True
|
ApiServer.__initialized = True
|
||||||
|
|
||||||
api_config = self._config['api_server']
|
api_config = self._config['api_server']
|
||||||
|
|
||||||
ApiServer._ws_channel_manager = ChannelManager()
|
|
||||||
|
|
||||||
self.app = FastAPI(title="Freqtrade API",
|
self.app = FastAPI(title="Freqtrade API",
|
||||||
docs_url='/docs' if api_config.get('enable_openapi', False) else None,
|
docs_url='/docs' if api_config.get('enable_openapi', False) else None,
|
||||||
redoc_url=None,
|
redoc_url=None,
|
||||||
@ -105,21 +94,9 @@ class ApiServer(RPCHandler):
|
|||||||
del ApiServer._rpc
|
del ApiServer._rpc
|
||||||
if self._server and not self._standalone:
|
if self._server and not self._standalone:
|
||||||
logger.info("Stopping API Server")
|
logger.info("Stopping API Server")
|
||||||
|
# self._server.force_exit, self._server.should_exit = True, True
|
||||||
self._server.cleanup()
|
self._server.cleanup()
|
||||||
|
|
||||||
if self._ws_thread and self._ws_loop:
|
|
||||||
logger.info("Stopping API Server background tasks")
|
|
||||||
|
|
||||||
if self._ws_background_task:
|
|
||||||
# Cancel the queue task
|
|
||||||
self._ws_background_task.cancel()
|
|
||||||
|
|
||||||
self._ws_thread.join()
|
|
||||||
|
|
||||||
self._ws_thread = None
|
|
||||||
self._ws_loop = None
|
|
||||||
self._ws_background_task = None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def shutdown(cls):
|
def shutdown(cls):
|
||||||
cls.__initialized = False
|
cls.__initialized = False
|
||||||
@ -129,9 +106,11 @@ class ApiServer(RPCHandler):
|
|||||||
cls._rpc = None
|
cls._rpc = None
|
||||||
|
|
||||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||||
if self._ws_queue:
|
"""
|
||||||
sync_q = self._ws_queue.sync_q
|
Publish the message to the message stream
|
||||||
sync_q.put(msg)
|
"""
|
||||||
|
if ApiServer._message_stream:
|
||||||
|
ApiServer._message_stream.publish(msg)
|
||||||
|
|
||||||
def handle_rpc_exception(self, request, exc):
|
def handle_rpc_exception(self, request, exc):
|
||||||
logger.exception(f"API Error calling: {exc}")
|
logger.exception(f"API Error calling: {exc}")
|
||||||
@ -170,51 +149,30 @@ class ApiServer(RPCHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
app.add_exception_handler(RPCException, self.handle_rpc_exception)
|
app.add_exception_handler(RPCException, self.handle_rpc_exception)
|
||||||
|
app.add_event_handler(
|
||||||
|
event_type="startup",
|
||||||
|
func=self._api_startup_event
|
||||||
|
)
|
||||||
|
app.add_event_handler(
|
||||||
|
event_type="shutdown",
|
||||||
|
func=self._api_shutdown_event
|
||||||
|
)
|
||||||
|
|
||||||
def start_message_queue(self):
|
async def _api_startup_event(self):
|
||||||
if self._ws_thread:
|
"""
|
||||||
return
|
Creates the MessageStream class on startup
|
||||||
|
so it has access to the same event loop
|
||||||
|
as uvicorn
|
||||||
|
"""
|
||||||
|
if not ApiServer._message_stream:
|
||||||
|
ApiServer._message_stream = MessageStream()
|
||||||
|
|
||||||
# Create a new loop, as it'll be just for the background thread
|
async def _api_shutdown_event(self):
|
||||||
self._ws_loop = asyncio.new_event_loop()
|
"""
|
||||||
|
Removes the MessageStream class on shutdown
|
||||||
# Start the thread
|
"""
|
||||||
self._ws_thread = Thread(target=self._ws_loop.run_forever)
|
if ApiServer._message_stream:
|
||||||
self._ws_thread.start()
|
ApiServer._message_stream = None
|
||||||
|
|
||||||
# Finally, submit the coro to the thread
|
|
||||||
self._ws_background_task = asyncio.run_coroutine_threadsafe(
|
|
||||||
self._broadcast_queue_data(), loop=self._ws_loop)
|
|
||||||
|
|
||||||
async def _broadcast_queue_data(self) -> None:
|
|
||||||
# Instantiate the queue in this coroutine so it's attached to our loop
|
|
||||||
self._ws_queue = ThreadedQueue()
|
|
||||||
async_queue = self._ws_queue.async_q
|
|
||||||
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
logger.debug("Getting queue messages...")
|
|
||||||
# Get data from queue
|
|
||||||
message: WSMessageSchemaType = await async_queue.get()
|
|
||||||
logger.debug(f"Found message of type: {message.get('type')}")
|
|
||||||
async_queue.task_done()
|
|
||||||
# Broadcast it
|
|
||||||
await self._ws_channel_manager.broadcast(message)
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# For testing, shouldn't happen when stable
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(f"Exception happened in background task: {e}")
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# Disconnect channels and stop the loop on cancel
|
|
||||||
await self._ws_channel_manager.disconnect_all()
|
|
||||||
if self._ws_loop:
|
|
||||||
self._ws_loop.stop()
|
|
||||||
# Avoid adding more items to the queue if they aren't
|
|
||||||
# going to get broadcasted.
|
|
||||||
self._ws_queue = None
|
|
||||||
|
|
||||||
def start_api(self):
|
def start_api(self):
|
||||||
"""
|
"""
|
||||||
@ -254,7 +212,6 @@ class ApiServer(RPCHandler):
|
|||||||
if self._standalone:
|
if self._standalone:
|
||||||
self._server.run()
|
self._server.run()
|
||||||
else:
|
else:
|
||||||
self.start_message_queue()
|
|
||||||
self._server.run_in_thread()
|
self._server.run_in_thread()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Api server failed to start.")
|
logger.exception("Api server failed to start.")
|
||||||
|
@ -3,4 +3,5 @@
|
|||||||
from freqtrade.rpc.api_server.ws.types import WebSocketType
|
from freqtrade.rpc.api_server.ws.types import WebSocketType
|
||||||
from freqtrade.rpc.api_server.ws.proxy import WebSocketProxy
|
from freqtrade.rpc.api_server.ws.proxy import WebSocketProxy
|
||||||
from freqtrade.rpc.api_server.ws.serializer import HybridJSONWebSocketSerializer
|
from freqtrade.rpc.api_server.ws.serializer import HybridJSONWebSocketSerializer
|
||||||
from freqtrade.rpc.api_server.ws.channel import ChannelManager, WebSocketChannel
|
from freqtrade.rpc.api_server.ws.channel import WebSocketChannel
|
||||||
|
from freqtrade.rpc.api_server.ws.message_stream import MessageStream
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from threading import RLock
|
from collections import deque
|
||||||
from typing import Any, Dict, List, Optional, Type, Union
|
from contextlib import asynccontextmanager
|
||||||
|
from typing import Any, AsyncIterator, Deque, Dict, List, Optional, Type, Union
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import WebSocket as FastAPIWebSocket
|
from fastapi import WebSocketDisconnect
|
||||||
|
from websockets.exceptions import ConnectionClosed
|
||||||
|
|
||||||
from freqtrade.rpc.api_server.ws.proxy import WebSocketProxy
|
from freqtrade.rpc.api_server.ws.proxy import WebSocketProxy
|
||||||
from freqtrade.rpc.api_server.ws.serializer import (HybridJSONWebSocketSerializer,
|
from freqtrade.rpc.api_server.ws.serializer import (HybridJSONWebSocketSerializer,
|
||||||
@ -21,31 +23,27 @@ class WebSocketChannel:
|
|||||||
"""
|
"""
|
||||||
Object to help facilitate managing a websocket connection
|
Object to help facilitate managing a websocket connection
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
websocket: WebSocketType,
|
websocket: WebSocketType,
|
||||||
channel_id: Optional[str] = None,
|
channel_id: Optional[str] = None,
|
||||||
drain_timeout: int = 3,
|
|
||||||
throttle: float = 0.01,
|
|
||||||
serializer_cls: Type[WebSocketSerializer] = HybridJSONWebSocketSerializer
|
serializer_cls: Type[WebSocketSerializer] = HybridJSONWebSocketSerializer
|
||||||
):
|
):
|
||||||
|
|
||||||
self.channel_id = channel_id if channel_id else uuid4().hex[:8]
|
self.channel_id = channel_id if channel_id else uuid4().hex[:8]
|
||||||
|
|
||||||
# The WebSocket object
|
|
||||||
self._websocket = WebSocketProxy(websocket)
|
self._websocket = WebSocketProxy(websocket)
|
||||||
|
|
||||||
self.drain_timeout = drain_timeout
|
|
||||||
self.throttle = throttle
|
|
||||||
|
|
||||||
self._subscriptions: List[str] = []
|
|
||||||
# 32 is the size of the receiving queue in websockets package
|
|
||||||
self.queue: asyncio.Queue[Dict[str, Any]] = asyncio.Queue(maxsize=32)
|
|
||||||
self._relay_task = asyncio.create_task(self.relay())
|
|
||||||
|
|
||||||
# Internal event to signify a closed websocket
|
# Internal event to signify a closed websocket
|
||||||
self._closed = asyncio.Event()
|
self._closed = asyncio.Event()
|
||||||
|
# The async tasks created for the channel
|
||||||
|
self._channel_tasks: List[asyncio.Task] = []
|
||||||
|
|
||||||
|
# Deque for average send times
|
||||||
|
self._send_times: Deque[float] = deque([], maxlen=10)
|
||||||
|
# High limit defaults to 3 to start
|
||||||
|
self._send_high_limit = 3
|
||||||
|
|
||||||
|
# The subscribed message types
|
||||||
|
self._subscriptions: List[str] = []
|
||||||
|
|
||||||
# Wrap the WebSocket in the Serializing class
|
# Wrap the WebSocket in the Serializing class
|
||||||
self._wrapped_ws = serializer_cls(self._websocket)
|
self._wrapped_ws = serializer_cls(self._websocket)
|
||||||
@ -61,40 +59,58 @@ class WebSocketChannel:
|
|||||||
def remote_addr(self):
|
def remote_addr(self):
|
||||||
return self._websocket.remote_addr
|
return self._websocket.remote_addr
|
||||||
|
|
||||||
async def _send(self, data):
|
@property
|
||||||
"""
|
def avg_send_time(self):
|
||||||
Send data on the wrapped websocket
|
return sum(self._send_times) / len(self._send_times)
|
||||||
"""
|
|
||||||
await self._wrapped_ws.send(data)
|
|
||||||
|
|
||||||
async def send(self, data) -> bool:
|
def _calc_send_limit(self):
|
||||||
"""
|
"""
|
||||||
Add the data to the queue to be sent.
|
Calculate the send high limit for this channel
|
||||||
:returns: True if data added to queue, False otherwise
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# This block only runs if the queue is full, it will wait
|
# Only update if we have enough data
|
||||||
# until self.drain_timeout for the relay to drain the outgoing queue
|
if len(self._send_times) == self._send_times.maxlen:
|
||||||
# We can't use asyncio.wait_for here because the queue may have been created with a
|
# At least 1s or twice the average of send times, with a
|
||||||
# different eventloop
|
# maximum of 3 seconds per message
|
||||||
start = time.time()
|
self._send_high_limit = min(max(self.avg_send_time * 2, 1), 3)
|
||||||
while self.queue.full():
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
if (time.time() - start) > self.drain_timeout:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# If for some reason the queue is still full, just return False
|
async def send(
|
||||||
|
self,
|
||||||
|
message: Union[WSMessageSchemaType, Dict[str, Any]],
|
||||||
|
timeout: bool = False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Send a message on the wrapped websocket. If the sending
|
||||||
|
takes too long, it will raise a TimeoutError and
|
||||||
|
disconnect the connection.
|
||||||
|
|
||||||
|
:param message: The message to send
|
||||||
|
:param timeout: Enforce send high limit, defaults to False
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
self.queue.put_nowait(data)
|
_ = time.time()
|
||||||
except asyncio.QueueFull:
|
# If the send times out, it will raise
|
||||||
return False
|
# a TimeoutError and bubble up to the
|
||||||
|
# message_endpoint to close the connection
|
||||||
|
await asyncio.wait_for(
|
||||||
|
self._wrapped_ws.send(message),
|
||||||
|
timeout=self._send_high_limit if timeout else None
|
||||||
|
)
|
||||||
|
total_time = time.time() - _
|
||||||
|
self._send_times.append(total_time)
|
||||||
|
|
||||||
# If we got here everything is ok
|
self._calc_send_limit()
|
||||||
return True
|
except asyncio.TimeoutError:
|
||||||
|
logger.info(f"Connection for {self} timed out, disconnecting")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Explicitly give control back to event loop as
|
||||||
|
# websockets.send does not
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
async def recv(self):
|
async def recv(self):
|
||||||
"""
|
"""
|
||||||
Receive data on the wrapped websocket
|
Receive a message on the wrapped websocket
|
||||||
"""
|
"""
|
||||||
return await self._wrapped_ws.recv()
|
return await self._wrapped_ws.recv()
|
||||||
|
|
||||||
@ -104,18 +120,28 @@ class WebSocketChannel:
|
|||||||
"""
|
"""
|
||||||
return await self._websocket.ping()
|
return await self._websocket.ping()
|
||||||
|
|
||||||
|
async def accept(self):
|
||||||
|
"""
|
||||||
|
Accept the underlying websocket connection,
|
||||||
|
if the connection has been closed before we can
|
||||||
|
accept, just close the channel.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return await self._websocket.accept()
|
||||||
|
except RuntimeError:
|
||||||
|
await self.close()
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
"""
|
"""
|
||||||
Close the WebSocketChannel
|
Close the WebSocketChannel
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
|
||||||
await self.raw_websocket.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self._closed.set()
|
self._closed.set()
|
||||||
self._relay_task.cancel()
|
|
||||||
|
try:
|
||||||
|
await self._websocket.close()
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
||||||
def is_closed(self) -> bool:
|
def is_closed(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -139,99 +165,76 @@ class WebSocketChannel:
|
|||||||
"""
|
"""
|
||||||
return message_type in self._subscriptions
|
return message_type in self._subscriptions
|
||||||
|
|
||||||
async def relay(self):
|
async def run_channel_tasks(self, *tasks, **kwargs):
|
||||||
"""
|
"""
|
||||||
Relay messages from the channel's queue and send them out. This is started
|
Create and await on the channel tasks unless an exception
|
||||||
as a task.
|
was raised, then cancel them all.
|
||||||
|
|
||||||
|
:params *tasks: All coros or tasks to be run concurrently
|
||||||
|
:param **kwargs: Any extra kwargs to pass to gather
|
||||||
"""
|
"""
|
||||||
while not self._closed.is_set():
|
|
||||||
message = await self.queue.get()
|
if not self.is_closed():
|
||||||
|
# Wrap the coros into tasks if they aren't already
|
||||||
|
self._channel_tasks = [
|
||||||
|
task if isinstance(task, asyncio.Task) else asyncio.create_task(task)
|
||||||
|
for task in tasks
|
||||||
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self._send(message)
|
return await asyncio.gather(*self._channel_tasks, **kwargs)
|
||||||
self.queue.task_done()
|
except Exception:
|
||||||
|
# If an exception occurred, cancel the rest of the tasks
|
||||||
|
await self.cancel_channel_tasks()
|
||||||
|
|
||||||
# Limit messages per sec.
|
async def cancel_channel_tasks(self):
|
||||||
# Could cause problems with queue size if too low, and
|
|
||||||
# problems with network traffik if too high.
|
|
||||||
# 0.01 = 100/s
|
|
||||||
await asyncio.sleep(self.throttle)
|
|
||||||
except RuntimeError:
|
|
||||||
# The connection was closed, just exit the task
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelManager:
|
|
||||||
def __init__(self):
|
|
||||||
self.channels = dict()
|
|
||||||
self._lock = RLock() # Re-entrant Lock
|
|
||||||
|
|
||||||
async def on_connect(self, websocket: WebSocketType):
|
|
||||||
"""
|
"""
|
||||||
Wrap websocket connection into Channel and add to list
|
Cancel and wait on all channel tasks
|
||||||
|
|
||||||
:param websocket: The WebSocket object to attach to the Channel
|
|
||||||
"""
|
"""
|
||||||
if isinstance(websocket, FastAPIWebSocket):
|
for task in self._channel_tasks:
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
# Wait for tasks to finish cancelling
|
||||||
try:
|
try:
|
||||||
await websocket.accept()
|
await task
|
||||||
except RuntimeError:
|
except (
|
||||||
# The connection was closed before we could accept it
|
asyncio.CancelledError,
|
||||||
return
|
asyncio.TimeoutError,
|
||||||
|
WebSocketDisconnect,
|
||||||
|
ConnectionClosed,
|
||||||
|
RuntimeError
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logger.info(f"Encountered unknown exception: {e}", exc_info=e)
|
||||||
|
|
||||||
ws_channel = WebSocketChannel(websocket)
|
self._channel_tasks = []
|
||||||
|
|
||||||
with self._lock:
|
async def __aiter__(self):
|
||||||
self.channels[websocket] = ws_channel
|
|
||||||
|
|
||||||
return ws_channel
|
|
||||||
|
|
||||||
async def on_disconnect(self, websocket: WebSocketType):
|
|
||||||
"""
|
"""
|
||||||
Call close on the channel if it's not, and remove from channel list
|
Generator for received messages
|
||||||
|
|
||||||
:param websocket: The WebSocket objet attached to the Channel
|
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
# We can not catch any errors here as websocket.recv is
|
||||||
channel = self.channels.get(websocket)
|
# the first to catch any disconnects and bubble it up
|
||||||
if channel:
|
# so the connection is garbage collected right away
|
||||||
logger.info(f"Disconnecting channel {channel}")
|
while not self.is_closed():
|
||||||
if not channel.is_closed():
|
yield await self.recv()
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def create_channel(
|
||||||
|
websocket: WebSocketType,
|
||||||
|
**kwargs
|
||||||
|
) -> AsyncIterator[WebSocketChannel]:
|
||||||
|
"""
|
||||||
|
Context manager for safely opening and closing a WebSocketChannel
|
||||||
|
"""
|
||||||
|
channel = WebSocketChannel(websocket, **kwargs)
|
||||||
|
try:
|
||||||
|
await channel.accept()
|
||||||
|
logger.info(f"Connected to channel - {channel}")
|
||||||
|
|
||||||
|
yield channel
|
||||||
|
finally:
|
||||||
await channel.close()
|
await channel.close()
|
||||||
|
logger.info(f"Disconnected from channel - {channel}")
|
||||||
del self.channels[websocket]
|
|
||||||
|
|
||||||
async def disconnect_all(self):
|
|
||||||
"""
|
|
||||||
Disconnect all Channels
|
|
||||||
"""
|
|
||||||
with self._lock:
|
|
||||||
for websocket in self.channels.copy().keys():
|
|
||||||
await self.on_disconnect(websocket)
|
|
||||||
|
|
||||||
async def broadcast(self, message: WSMessageSchemaType):
|
|
||||||
"""
|
|
||||||
Broadcast a message on all Channels
|
|
||||||
|
|
||||||
:param message: The message to send
|
|
||||||
"""
|
|
||||||
with self._lock:
|
|
||||||
for channel in self.channels.copy().values():
|
|
||||||
if channel.subscribed_to(message.get('type')):
|
|
||||||
await self.send_direct(channel, message)
|
|
||||||
|
|
||||||
async def send_direct(
|
|
||||||
self, channel: WebSocketChannel, message: Union[WSMessageSchemaType, Dict[str, Any]]):
|
|
||||||
"""
|
|
||||||
Send a message directly through direct_channel only
|
|
||||||
|
|
||||||
:param direct_channel: The WebSocketChannel object to send the message through
|
|
||||||
:param message: The message to send
|
|
||||||
"""
|
|
||||||
if not await channel.send(message):
|
|
||||||
await self.on_disconnect(channel.raw_websocket)
|
|
||||||
|
|
||||||
def has_channels(self):
|
|
||||||
"""
|
|
||||||
Flag for more than 0 channels
|
|
||||||
"""
|
|
||||||
return len(self.channels) > 0
|
|
||||||
|
31
freqtrade/rpc/api_server/ws/message_stream.py
Normal file
31
freqtrade/rpc/api_server/ws/message_stream.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class MessageStream:
|
||||||
|
"""
|
||||||
|
A message stream for consumers to subscribe to,
|
||||||
|
and for producers to publish to.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self._loop = asyncio.get_running_loop()
|
||||||
|
self._waiter = self._loop.create_future()
|
||||||
|
|
||||||
|
def publish(self, message):
|
||||||
|
"""
|
||||||
|
Publish a message to this MessageStream
|
||||||
|
|
||||||
|
:param message: The message to publish
|
||||||
|
"""
|
||||||
|
waiter, self._waiter = self._waiter, self._loop.create_future()
|
||||||
|
waiter.set_result((message, time.time(), self._waiter))
|
||||||
|
|
||||||
|
async def __aiter__(self):
|
||||||
|
"""
|
||||||
|
Iterate over the messages in the message stream
|
||||||
|
"""
|
||||||
|
waiter = self._waiter
|
||||||
|
while True:
|
||||||
|
# Shield the future from being cancelled by a task waiting on it
|
||||||
|
message, ts, waiter = await asyncio.shield(waiter)
|
||||||
|
yield message, ts
|
@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Any, Dict, Union
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
import rapidjson
|
import rapidjson
|
||||||
@ -7,6 +8,7 @@ from pandas import DataFrame
|
|||||||
|
|
||||||
from freqtrade.misc import dataframe_to_json, json_to_dataframe
|
from freqtrade.misc import dataframe_to_json, json_to_dataframe
|
||||||
from freqtrade.rpc.api_server.ws.proxy import WebSocketProxy
|
from freqtrade.rpc.api_server.ws.proxy import WebSocketProxy
|
||||||
|
from freqtrade.rpc.api_server.ws_schemas import WSMessageSchemaType
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -24,17 +26,13 @@ class WebSocketSerializer(ABC):
|
|||||||
def _deserialize(self, data):
|
def _deserialize(self, data):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def send(self, data: bytes):
|
async def send(self, data: Union[WSMessageSchemaType, Dict[str, Any]]):
|
||||||
await self._websocket.send(self._serialize(data))
|
await self._websocket.send(self._serialize(data))
|
||||||
|
|
||||||
async def recv(self) -> bytes:
|
async def recv(self) -> bytes:
|
||||||
data = await self._websocket.recv()
|
data = await self._websocket.recv()
|
||||||
|
|
||||||
return self._deserialize(data)
|
return self._deserialize(data)
|
||||||
|
|
||||||
async def close(self, code: int = 1000):
|
|
||||||
await self._websocket.close(code)
|
|
||||||
|
|
||||||
|
|
||||||
class HybridJSONWebSocketSerializer(WebSocketSerializer):
|
class HybridJSONWebSocketSerializer(WebSocketSerializer):
|
||||||
def _serialize(self, data) -> str:
|
def _serialize(self, data) -> str:
|
||||||
|
@ -31,6 +31,7 @@ class Producer(TypedDict):
|
|||||||
name: str
|
name: str
|
||||||
host: str
|
host: str
|
||||||
port: int
|
port: int
|
||||||
|
secure: bool
|
||||||
ws_token: str
|
ws_token: str
|
||||||
|
|
||||||
|
|
||||||
@ -180,7 +181,8 @@ class ExternalMessageConsumer:
|
|||||||
host, port = producer['host'], producer['port']
|
host, port = producer['host'], producer['port']
|
||||||
token = producer['ws_token']
|
token = producer['ws_token']
|
||||||
name = producer['name']
|
name = producer['name']
|
||||||
ws_url = f"ws://{host}:{port}/api/v1/message/ws?token={token}"
|
scheme = 'wss' if producer.get('secure', False) else 'ws'
|
||||||
|
ws_url = f"{scheme}://{host}:{port}/api/v1/message/ws?token={token}"
|
||||||
|
|
||||||
# This will raise InvalidURI if the url is bad
|
# This will raise InvalidURI if the url is bad
|
||||||
async with websockets.connect(
|
async with websockets.connect(
|
||||||
|
@ -218,9 +218,10 @@ class RPC:
|
|||||||
stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2),
|
stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2),
|
||||||
stoploss_entry_dist=stoploss_entry_dist,
|
stoploss_entry_dist=stoploss_entry_dist,
|
||||||
stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8),
|
stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8),
|
||||||
open_order='({} {} rem={:.8f})'.format(
|
open_order=(
|
||||||
order.order_type, order.side, order.remaining
|
f'({order.order_type} {order.side} rem={order.safe_remaining:.8f})' if
|
||||||
) if order else None,
|
order else None
|
||||||
|
),
|
||||||
))
|
))
|
||||||
results.append(trade_dict)
|
results.append(trade_dict)
|
||||||
return results
|
return results
|
||||||
@ -739,6 +740,24 @@ class RPC:
|
|||||||
self._freqtrade.wallets.update()
|
self._freqtrade.wallets.update()
|
||||||
return {'result': f'Created sell order for trade {trade_id}.'}
|
return {'result': f'Created sell order for trade {trade_id}.'}
|
||||||
|
|
||||||
|
def _force_entry_validations(self, pair: str, order_side: SignalDirection):
|
||||||
|
if not self._freqtrade.config.get('force_entry_enable', False):
|
||||||
|
raise RPCException('Force_entry not enabled.')
|
||||||
|
|
||||||
|
if self._freqtrade.state != State.RUNNING:
|
||||||
|
raise RPCException('trader is not running')
|
||||||
|
|
||||||
|
if order_side == SignalDirection.SHORT and self._freqtrade.trading_mode == TradingMode.SPOT:
|
||||||
|
raise RPCException("Can't go short on Spot markets.")
|
||||||
|
|
||||||
|
if pair not in self._freqtrade.exchange.get_markets(tradable_only=True):
|
||||||
|
raise RPCException('Symbol does not exist or market is not active.')
|
||||||
|
# Check if pair quote currency equals to the stake currency.
|
||||||
|
stake_currency = self._freqtrade.config.get('stake_currency')
|
||||||
|
if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency:
|
||||||
|
raise RPCException(
|
||||||
|
f'Wrong pair selected. Only pairs with stake-currency {stake_currency} allowed.')
|
||||||
|
|
||||||
def _rpc_force_entry(self, pair: str, price: Optional[float], *,
|
def _rpc_force_entry(self, pair: str, price: Optional[float], *,
|
||||||
order_type: Optional[str] = None,
|
order_type: Optional[str] = None,
|
||||||
order_side: SignalDirection = SignalDirection.LONG,
|
order_side: SignalDirection = SignalDirection.LONG,
|
||||||
@ -749,21 +768,8 @@ class RPC:
|
|||||||
Handler for forcebuy <asset> <price>
|
Handler for forcebuy <asset> <price>
|
||||||
Buys a pair trade at the given or current price
|
Buys a pair trade at the given or current price
|
||||||
"""
|
"""
|
||||||
|
self._force_entry_validations(pair, order_side)
|
||||||
|
|
||||||
if not self._freqtrade.config.get('force_entry_enable', False):
|
|
||||||
raise RPCException('Force_entry not enabled.')
|
|
||||||
|
|
||||||
if self._freqtrade.state != State.RUNNING:
|
|
||||||
raise RPCException('trader is not running')
|
|
||||||
|
|
||||||
if order_side == SignalDirection.SHORT and self._freqtrade.trading_mode == TradingMode.SPOT:
|
|
||||||
raise RPCException("Can't go short on Spot markets.")
|
|
||||||
|
|
||||||
# Check if pair quote currency equals to the stake currency.
|
|
||||||
stake_currency = self._freqtrade.config.get('stake_currency')
|
|
||||||
if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency:
|
|
||||||
raise RPCException(
|
|
||||||
f'Wrong pair selected. Only pairs with stake-currency {stake_currency} allowed.')
|
|
||||||
# check if valid pair
|
# check if valid pair
|
||||||
|
|
||||||
# check if pair already has an open pair
|
# check if pair already has an open pair
|
||||||
@ -773,6 +779,9 @@ class RPC:
|
|||||||
is_short = trade.is_short
|
is_short = trade.is_short
|
||||||
if not self._freqtrade.strategy.position_adjustment_enable:
|
if not self._freqtrade.strategy.position_adjustment_enable:
|
||||||
raise RPCException(f'position for {pair} already open - id: {trade.id}')
|
raise RPCException(f'position for {pair} already open - id: {trade.id}')
|
||||||
|
if trade.open_order_id is not None:
|
||||||
|
raise RPCException(f'position for {pair} already open - id: {trade.id} '
|
||||||
|
f'and has open order {trade.open_order_id}')
|
||||||
else:
|
else:
|
||||||
if Trade.get_open_trade_count() >= self._config['max_open_trades']:
|
if Trade.get_open_trade_count() >= self._config['max_open_trades']:
|
||||||
raise RPCException("Maximum number of trades is reached.")
|
raise RPCException("Maximum number of trades is reached.")
|
||||||
@ -785,6 +794,7 @@ class RPC:
|
|||||||
if not order_type:
|
if not order_type:
|
||||||
order_type = self._freqtrade.strategy.order_types.get(
|
order_type = self._freqtrade.strategy.order_types.get(
|
||||||
'force_entry', self._freqtrade.strategy.order_types['entry'])
|
'force_entry', self._freqtrade.strategy.order_types['entry'])
|
||||||
|
with self._freqtrade._exit_lock:
|
||||||
if self._freqtrade.execute_entry(pair, stake_amount, price,
|
if self._freqtrade.execute_entry(pair, stake_amount, price,
|
||||||
ordertype=order_type, trade=trade,
|
ordertype=order_type, trade=trade,
|
||||||
is_short=is_short,
|
is_short=is_short,
|
||||||
|
@ -79,6 +79,8 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
return command_handler(self, *args, **kwargs)
|
return command_handler(self, *args, **kwargs)
|
||||||
|
except RPCException as e:
|
||||||
|
self._send_msg(str(e))
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.exception('Exception occurred within Telegram module')
|
logger.exception('Exception occurred within Telegram module')
|
||||||
|
|
||||||
@ -538,8 +540,6 @@ class Telegram(RPCHandler):
|
|||||||
handler for `/status` and `/status <id>`.
|
handler for `/status` and `/status <id>`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
|
|
||||||
# Check if there's at least one numerical ID provided.
|
# Check if there's at least one numerical ID provided.
|
||||||
# If so, try to get only these trades.
|
# If so, try to get only these trades.
|
||||||
trade_ids = []
|
trade_ids = []
|
||||||
@ -602,9 +602,6 @@ class Telegram(RPCHandler):
|
|||||||
lines.extend(lines_detail if lines_detail else "")
|
lines.extend(lines_detail if lines_detail else "")
|
||||||
self.__send_status_msg(lines, r)
|
self.__send_status_msg(lines, r)
|
||||||
|
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None:
|
def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Send status message.
|
Send status message.
|
||||||
@ -630,7 +627,6 @@ class Telegram(RPCHandler):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
fiat_currency = self._config.get('fiat_display_currency', '')
|
fiat_currency = self._config.get('fiat_display_currency', '')
|
||||||
statlist, head, fiat_profit_sum = self._rpc._rpc_status_table(
|
statlist, head, fiat_profit_sum = self._rpc._rpc_status_table(
|
||||||
self._config['stake_currency'], fiat_currency)
|
self._config['stake_currency'], fiat_currency)
|
||||||
@ -659,8 +655,6 @@ class Telegram(RPCHandler):
|
|||||||
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML,
|
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML,
|
||||||
reload_able=True, callback_path="update_status_table",
|
reload_able=True, callback_path="update_status_table",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None:
|
def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None:
|
||||||
@ -686,7 +680,6 @@ class Telegram(RPCHandler):
|
|||||||
timescale = int(context.args[0]) if context.args else val.default
|
timescale = int(context.args[0]) if context.args else val.default
|
||||||
except (TypeError, ValueError, IndexError):
|
except (TypeError, ValueError, IndexError):
|
||||||
timescale = val.default
|
timescale = val.default
|
||||||
try:
|
|
||||||
stats = self._rpc._rpc_timeunit_profit(
|
stats = self._rpc._rpc_timeunit_profit(
|
||||||
timescale,
|
timescale,
|
||||||
stake_cur,
|
stake_cur,
|
||||||
@ -713,8 +706,6 @@ class Telegram(RPCHandler):
|
|||||||
)
|
)
|
||||||
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
|
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
|
||||||
callback_path=val.callback, query=update.callback_query)
|
callback_path=val.callback, query=update.callback_query)
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _daily(self, update: Update, context: CallbackContext) -> None:
|
def _daily(self, update: Update, context: CallbackContext) -> None:
|
||||||
@ -878,7 +869,6 @@ class Telegram(RPCHandler):
|
|||||||
@authorized_only
|
@authorized_only
|
||||||
def _balance(self, update: Update, context: CallbackContext) -> None:
|
def _balance(self, update: Update, context: CallbackContext) -> None:
|
||||||
""" Handler for /balance """
|
""" Handler for /balance """
|
||||||
try:
|
|
||||||
result = self._rpc._rpc_balance(self._config['stake_currency'],
|
result = self._rpc._rpc_balance(self._config['stake_currency'],
|
||||||
self._config.get('fiat_display_currency', ''))
|
self._config.get('fiat_display_currency', ''))
|
||||||
|
|
||||||
@ -949,8 +939,6 @@ class Telegram(RPCHandler):
|
|||||||
f"{fiat_val}\n")
|
f"{fiat_val}\n")
|
||||||
self._send_msg(output, reload_able=True, callback_path="update_balance",
|
self._send_msg(output, reload_able=True, callback_path="update_balance",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _start(self, update: Update, context: CallbackContext) -> None:
|
def _start(self, update: Update, context: CallbackContext) -> None:
|
||||||
@ -1125,7 +1113,6 @@ class Telegram(RPCHandler):
|
|||||||
nrecent = int(context.args[0]) if context.args else 10
|
nrecent = int(context.args[0]) if context.args else 10
|
||||||
except (TypeError, ValueError, IndexError):
|
except (TypeError, ValueError, IndexError):
|
||||||
nrecent = 10
|
nrecent = 10
|
||||||
try:
|
|
||||||
trades = self._rpc._rpc_trade_history(
|
trades = self._rpc._rpc_trade_history(
|
||||||
nrecent
|
nrecent
|
||||||
)
|
)
|
||||||
@ -1143,8 +1130,6 @@ class Telegram(RPCHandler):
|
|||||||
message = (f"<b>{min(trades['trades_count'], nrecent)} recent trades</b>:\n"
|
message = (f"<b>{min(trades['trades_count'], nrecent)} recent trades</b>:\n"
|
||||||
+ (f"<pre>{trades_tab}</pre>" if trades['trades_count'] > 0 else ''))
|
+ (f"<pre>{trades_tab}</pre>" if trades['trades_count'] > 0 else ''))
|
||||||
self._send_msg(message, parse_mode=ParseMode.HTML)
|
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _delete_trade(self, update: Update, context: CallbackContext) -> None:
|
def _delete_trade(self, update: Update, context: CallbackContext) -> None:
|
||||||
@ -1155,7 +1140,6 @@ class Telegram(RPCHandler):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
if not context.args or len(context.args) == 0:
|
if not context.args or len(context.args) == 0:
|
||||||
raise RPCException("Trade-id not set.")
|
raise RPCException("Trade-id not set.")
|
||||||
trade_id = int(context.args[0])
|
trade_id = int(context.args[0])
|
||||||
@ -1165,9 +1149,6 @@ class Telegram(RPCHandler):
|
|||||||
'Please make sure to take care of this asset on the exchange manually.'
|
'Please make sure to take care of this asset on the exchange manually.'
|
||||||
))
|
))
|
||||||
|
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _performance(self, update: Update, context: CallbackContext) -> None:
|
def _performance(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1177,7 +1158,6 @@ class Telegram(RPCHandler):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
trades = self._rpc._rpc_performance()
|
trades = self._rpc._rpc_performance()
|
||||||
output = "<b>Performance:</b>\n"
|
output = "<b>Performance:</b>\n"
|
||||||
for i, trade in enumerate(trades):
|
for i, trade in enumerate(trades):
|
||||||
@ -1196,8 +1176,6 @@ class Telegram(RPCHandler):
|
|||||||
self._send_msg(output, parse_mode=ParseMode.HTML,
|
self._send_msg(output, parse_mode=ParseMode.HTML,
|
||||||
reload_able=True, callback_path="update_performance",
|
reload_able=True, callback_path="update_performance",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None:
|
def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None:
|
||||||
@ -1208,7 +1186,6 @@ class Telegram(RPCHandler):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
pair = None
|
pair = None
|
||||||
if context.args and isinstance(context.args[0], str):
|
if context.args and isinstance(context.args[0], str):
|
||||||
pair = context.args[0]
|
pair = context.args[0]
|
||||||
@ -1231,8 +1208,6 @@ class Telegram(RPCHandler):
|
|||||||
self._send_msg(output, parse_mode=ParseMode.HTML,
|
self._send_msg(output, parse_mode=ParseMode.HTML,
|
||||||
reload_able=True, callback_path="update_enter_tag_performance",
|
reload_able=True, callback_path="update_enter_tag_performance",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None:
|
def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None:
|
||||||
@ -1243,7 +1218,6 @@ class Telegram(RPCHandler):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
pair = None
|
pair = None
|
||||||
if context.args and isinstance(context.args[0], str):
|
if context.args and isinstance(context.args[0], str):
|
||||||
pair = context.args[0]
|
pair = context.args[0]
|
||||||
@ -1266,8 +1240,6 @@ class Telegram(RPCHandler):
|
|||||||
self._send_msg(output, parse_mode=ParseMode.HTML,
|
self._send_msg(output, parse_mode=ParseMode.HTML,
|
||||||
reload_able=True, callback_path="update_exit_reason_performance",
|
reload_able=True, callback_path="update_exit_reason_performance",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None:
|
def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None:
|
||||||
@ -1278,7 +1250,6 @@ class Telegram(RPCHandler):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
pair = None
|
pair = None
|
||||||
if context.args and isinstance(context.args[0], str):
|
if context.args and isinstance(context.args[0], str):
|
||||||
pair = context.args[0]
|
pair = context.args[0]
|
||||||
@ -1301,8 +1272,6 @@ class Telegram(RPCHandler):
|
|||||||
self._send_msg(output, parse_mode=ParseMode.HTML,
|
self._send_msg(output, parse_mode=ParseMode.HTML,
|
||||||
reload_able=True, callback_path="update_mix_tag_performance",
|
reload_able=True, callback_path="update_mix_tag_performance",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _count(self, update: Update, context: CallbackContext) -> None:
|
def _count(self, update: Update, context: CallbackContext) -> None:
|
||||||
@ -1313,7 +1282,6 @@ class Telegram(RPCHandler):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
counts = self._rpc._rpc_count()
|
counts = self._rpc._rpc_count()
|
||||||
message = tabulate({k: [v] for k, v in counts.items()},
|
message = tabulate({k: [v] for k, v in counts.items()},
|
||||||
headers=['current', 'max', 'total stake'],
|
headers=['current', 'max', 'total stake'],
|
||||||
@ -1323,8 +1291,6 @@ class Telegram(RPCHandler):
|
|||||||
self._send_msg(message, parse_mode=ParseMode.HTML,
|
self._send_msg(message, parse_mode=ParseMode.HTML,
|
||||||
reload_able=True, callback_path="update_count",
|
reload_able=True, callback_path="update_count",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _locks(self, update: Update, context: CallbackContext) -> None:
|
def _locks(self, update: Update, context: CallbackContext) -> None:
|
||||||
@ -1372,7 +1338,6 @@ class Telegram(RPCHandler):
|
|||||||
Handler for /whitelist
|
Handler for /whitelist
|
||||||
Shows the currently active whitelist
|
Shows the currently active whitelist
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
whitelist = self._rpc._rpc_whitelist()
|
whitelist = self._rpc._rpc_whitelist()
|
||||||
|
|
||||||
if context.args:
|
if context.args:
|
||||||
@ -1386,8 +1351,6 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
logger.debug(message)
|
logger.debug(message)
|
||||||
self._send_msg(message)
|
self._send_msg(message)
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _blacklist(self, update: Update, context: CallbackContext) -> None:
|
def _blacklist(self, update: Update, context: CallbackContext) -> None:
|
||||||
@ -1424,7 +1387,6 @@ class Telegram(RPCHandler):
|
|||||||
Handler for /logs
|
Handler for /logs
|
||||||
Shows the latest logs
|
Shows the latest logs
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
try:
|
try:
|
||||||
limit = int(context.args[0]) if context.args else 10
|
limit = int(context.args[0]) if context.args else 10
|
||||||
except (TypeError, ValueError, IndexError):
|
except (TypeError, ValueError, IndexError):
|
||||||
@ -1447,8 +1409,6 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
if msgs:
|
if msgs:
|
||||||
self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
|
self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _edge(self, update: Update, context: CallbackContext) -> None:
|
def _edge(self, update: Update, context: CallbackContext) -> None:
|
||||||
@ -1456,7 +1416,6 @@ class Telegram(RPCHandler):
|
|||||||
Handler for /edge
|
Handler for /edge
|
||||||
Shows information related to Edge
|
Shows information related to Edge
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
edge_pairs = self._rpc._rpc_edge()
|
edge_pairs = self._rpc._rpc_edge()
|
||||||
if not edge_pairs:
|
if not edge_pairs:
|
||||||
message = '<b>Edge only validated following pairs:</b>'
|
message = '<b>Edge only validated following pairs:</b>'
|
||||||
@ -1469,9 +1428,6 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
self._send_msg(message, parse_mode=ParseMode.HTML)
|
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _help(self, update: Update, context: CallbackContext) -> None:
|
def _help(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1551,12 +1507,9 @@ class Telegram(RPCHandler):
|
|||||||
Handler for /health
|
Handler for /health
|
||||||
Shows the last process timestamp
|
Shows the last process timestamp
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
health = self._rpc._health()
|
health = self._rpc._health()
|
||||||
message = f"Last process: `{health['last_process_loc']}`"
|
message = f"Last process: `{health['last_process_loc']}`"
|
||||||
self._send_msg(message)
|
self._send_msg(message)
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _version(self, update: Update, context: CallbackContext) -> None:
|
def _version(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
@ -19,7 +19,7 @@ class FreqaiExampleHybridStrategy(IStrategy):
|
|||||||
|
|
||||||
Launching this strategy would be:
|
Launching this strategy would be:
|
||||||
|
|
||||||
freqtrade trade --strategy FreqaiExampleHyridStrategy --strategy-path freqtrade/templates
|
freqtrade trade --strategy FreqaiExampleHybridStrategy --strategy-path freqtrade/templates
|
||||||
--freqaimodel CatboostClassifier --config config_examples/config_freqai.example.json
|
--freqaimodel CatboostClassifier --config config_examples/config_freqai.example.json
|
||||||
|
|
||||||
or the user simply adds this to their config:
|
or the user simply adds this to their config:
|
||||||
@ -86,7 +86,7 @@ class FreqaiExampleHybridStrategy(IStrategy):
|
|||||||
process_only_new_candles = True
|
process_only_new_candles = True
|
||||||
stoploss = -0.05
|
stoploss = -0.05
|
||||||
use_exit_signal = True
|
use_exit_signal = True
|
||||||
startup_candle_count: int = 300
|
startup_candle_count: int = 30
|
||||||
can_short = True
|
can_short = True
|
||||||
|
|
||||||
# Hyperoptable parameters
|
# Hyperoptable parameters
|
||||||
|
@ -328,7 +328,7 @@
|
|||||||
"# Show graph inline\n",
|
"# Show graph inline\n",
|
||||||
"# graph.show()\n",
|
"# graph.show()\n",
|
||||||
"\n",
|
"\n",
|
||||||
"# Render graph in a seperate window\n",
|
"# Render graph in a separate window\n",
|
||||||
"graph.show(renderer=\"browser\")\n"
|
"graph.show(renderer=\"browser\")\n"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -29,6 +29,7 @@ nav:
|
|||||||
- Parameter table: freqai-parameter-table.md
|
- Parameter table: freqai-parameter-table.md
|
||||||
- Feature engineering: freqai-feature-engineering.md
|
- Feature engineering: freqai-feature-engineering.md
|
||||||
- Running FreqAI: freqai-running.md
|
- Running FreqAI: freqai-running.md
|
||||||
|
- Reinforcement Learning: freqai-reinforcement-learning.md
|
||||||
- Developer guide: freqai-developers.md
|
- Developer guide: freqai-developers.md
|
||||||
- Short / Leverage: leverage.md
|
- Short / Leverage: leverage.md
|
||||||
- Utility Sub-commands: utils.md
|
- Utility Sub-commands: utils.md
|
||||||
|
@ -3,12 +3,13 @@
|
|||||||
-r requirements-plot.txt
|
-r requirements-plot.txt
|
||||||
-r requirements-hyperopt.txt
|
-r requirements-hyperopt.txt
|
||||||
-r requirements-freqai.txt
|
-r requirements-freqai.txt
|
||||||
|
-r requirements-freqai-rl.txt
|
||||||
-r docs/requirements-docs.txt
|
-r docs/requirements-docs.txt
|
||||||
|
|
||||||
coveralls==3.3.1
|
coveralls==3.3.1
|
||||||
flake8==5.0.4
|
flake8==6.0.0
|
||||||
flake8-tidy-imports==4.8.0
|
flake8-tidy-imports==4.8.0
|
||||||
mypy==0.990
|
mypy==0.991
|
||||||
pre-commit==2.20.0
|
pre-commit==2.20.0
|
||||||
pytest==7.2.0
|
pytest==7.2.0
|
||||||
pytest-asyncio==0.20.2
|
pytest-asyncio==0.20.2
|
||||||
@ -19,14 +20,14 @@ isort==5.10.1
|
|||||||
# For datetime mocking
|
# For datetime mocking
|
||||||
time-machine==2.8.2
|
time-machine==2.8.2
|
||||||
# fastapi testing
|
# fastapi testing
|
||||||
httpx==0.23.0
|
httpx==0.23.1
|
||||||
|
|
||||||
# Convert jupyter notebooks to markdown documents
|
# Convert jupyter notebooks to markdown documents
|
||||||
nbconvert==7.2.4
|
nbconvert==7.2.5
|
||||||
|
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==5.2.1
|
types-cachetools==5.2.1
|
||||||
types-filelock==3.2.7
|
types-filelock==3.2.7
|
||||||
types-requests==2.28.11.4
|
types-requests==2.28.11.5
|
||||||
types-tabulate==0.9.0.0
|
types-tabulate==0.9.0.0
|
||||||
types-python-dateutil==2.8.19.3
|
types-python-dateutil==2.8.19.4
|
||||||
|
9
requirements-freqai-rl.txt
Normal file
9
requirements-freqai-rl.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Include all requirements to run the bot.
|
||||||
|
-r requirements-freqai.txt
|
||||||
|
|
||||||
|
# Required for freqai-rl
|
||||||
|
torch==1.12.1
|
||||||
|
stable-baselines3==1.6.2
|
||||||
|
sb3-contrib==1.6.2
|
||||||
|
# Gym is forced to this version by stable-baselines3.
|
||||||
|
gym==0.21
|
@ -1,19 +1,19 @@
|
|||||||
numpy==1.23.4
|
numpy==1.23.5
|
||||||
pandas==1.5.1
|
pandas==1.5.1
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==2.1.75
|
ccxt==2.2.36
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==38.0.1; platform_machine == 'armv7l'
|
cryptography==38.0.1; platform_machine == 'armv7l'
|
||||||
cryptography==38.0.3; platform_machine != 'armv7l'
|
cryptography==38.0.4; platform_machine != 'armv7l'
|
||||||
aiohttp==3.8.3
|
aiohttp==3.8.3
|
||||||
SQLAlchemy==1.4.44
|
SQLAlchemy==1.4.44
|
||||||
python-telegram-bot==13.14
|
python-telegram-bot==13.14
|
||||||
arrow==1.2.3
|
arrow==1.2.3
|
||||||
cachetools==4.2.2
|
cachetools==4.2.2
|
||||||
requests==2.28.1
|
requests==2.28.1
|
||||||
urllib3==1.26.12
|
urllib3==1.26.13
|
||||||
jsonschema==4.17.0
|
jsonschema==4.17.1
|
||||||
TA-Lib==0.4.25
|
TA-Lib==0.4.25
|
||||||
technical==1.3.0
|
technical==1.3.0
|
||||||
tabulate==0.9.0
|
tabulate==0.9.0
|
||||||
@ -22,7 +22,7 @@ jinja2==3.1.2
|
|||||||
tables==3.7.0
|
tables==3.7.0
|
||||||
blosc==1.10.6
|
blosc==1.10.6
|
||||||
joblib==1.2.0
|
joblib==1.2.0
|
||||||
pyarrow==10.0.0; platform_machine != 'armv7l'
|
pyarrow==10.0.1; platform_machine != 'armv7l'
|
||||||
|
|
||||||
# find first, C search in arrays
|
# find first, C search in arrays
|
||||||
py_find_1st==1.1.5
|
py_find_1st==1.1.5
|
||||||
@ -30,7 +30,7 @@ py_find_1st==1.1.5
|
|||||||
# Load ticker files 30% faster
|
# Load ticker files 30% faster
|
||||||
python-rapidjson==1.9
|
python-rapidjson==1.9
|
||||||
# Properly format api responses
|
# Properly format api responses
|
||||||
orjson==3.8.1
|
orjson==3.8.2
|
||||||
|
|
||||||
# Notify systemd
|
# Notify systemd
|
||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
@ -38,7 +38,7 @@ sdnotify==0.3.2
|
|||||||
# API Server
|
# API Server
|
||||||
fastapi==0.87.0
|
fastapi==0.87.0
|
||||||
pydantic==1.10.2
|
pydantic==1.10.2
|
||||||
uvicorn==0.19.0
|
uvicorn==0.20.0
|
||||||
pyjwt==2.6.0
|
pyjwt==2.6.0
|
||||||
aiofiles==22.1.0
|
aiofiles==22.1.0
|
||||||
psutil==5.9.4
|
psutil==5.9.4
|
||||||
@ -47,7 +47,7 @@ psutil==5.9.4
|
|||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
# Building config files interactively
|
# Building config files interactively
|
||||||
questionary==1.10.0
|
questionary==1.10.0
|
||||||
prompt-toolkit==3.0.32
|
prompt-toolkit==3.0.33
|
||||||
# Extensions to datetime library
|
# Extensions to datetime library
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
|
|
||||||
|
@ -199,6 +199,7 @@ async def create_client(
|
|||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
token,
|
token,
|
||||||
|
scheme='ws',
|
||||||
name='default',
|
name='default',
|
||||||
protocol=ClientProtocol(),
|
protocol=ClientProtocol(),
|
||||||
sleep_time=10,
|
sleep_time=10,
|
||||||
@ -211,13 +212,14 @@ async def create_client(
|
|||||||
:param host: The host
|
:param host: The host
|
||||||
:param port: The port
|
:param port: The port
|
||||||
:param token: The websocket auth token
|
:param token: The websocket auth token
|
||||||
|
:param scheme: `ws` for most connections, `wss` for ssl
|
||||||
:param name: The name of the producer
|
:param name: The name of the producer
|
||||||
:param **kwargs: Any extra kwargs passed to websockets.connect
|
:param **kwargs: Any extra kwargs passed to websockets.connect
|
||||||
"""
|
"""
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
try:
|
try:
|
||||||
websocket_url = f"ws://{host}:{port}/api/v1/message/ws?token={token}"
|
websocket_url = f"{scheme}://{host}:{port}/api/v1/message/ws?token={token}"
|
||||||
logger.info(f"Attempting to connect to {name} @ {host}:{port}")
|
logger.info(f"Attempting to connect to {name} @ {host}:{port}")
|
||||||
|
|
||||||
async with websockets.connect(websocket_url, **kwargs) as ws:
|
async with websockets.connect(websocket_url, **kwargs) as ws:
|
||||||
@ -304,6 +306,7 @@ async def _main(args):
|
|||||||
producer['host'],
|
producer['host'],
|
||||||
producer['port'],
|
producer['port'],
|
||||||
producer['ws_token'],
|
producer['ws_token'],
|
||||||
|
'wss' if producer.get('secure', False) else 'ws',
|
||||||
producer['name'],
|
producer['name'],
|
||||||
sleep_time=sleep_time,
|
sleep_time=sleep_time,
|
||||||
ping_timeout=ping_timeout,
|
ping_timeout=ping_timeout,
|
||||||
|
11
setup.py
11
setup.py
@ -15,6 +15,14 @@ freqai = [
|
|||||||
'scikit-learn',
|
'scikit-learn',
|
||||||
'catboost; platform_machine != "aarch64"',
|
'catboost; platform_machine != "aarch64"',
|
||||||
'lightgbm',
|
'lightgbm',
|
||||||
|
'xgboost'
|
||||||
|
]
|
||||||
|
|
||||||
|
freqai_rl = [
|
||||||
|
'torch',
|
||||||
|
'stable-baselines3',
|
||||||
|
'gym==0.21',
|
||||||
|
'sb3-contrib'
|
||||||
]
|
]
|
||||||
|
|
||||||
develop = [
|
develop = [
|
||||||
@ -36,7 +44,7 @@ jupyter = [
|
|||||||
'nbconvert',
|
'nbconvert',
|
||||||
]
|
]
|
||||||
|
|
||||||
all_extra = plot + develop + jupyter + hyperopt + freqai
|
all_extra = plot + develop + jupyter + hyperopt + freqai + freqai_rl
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
tests_require=[
|
tests_require=[
|
||||||
@ -90,6 +98,7 @@ setup(
|
|||||||
'jupyter': jupyter,
|
'jupyter': jupyter,
|
||||||
'hyperopt': hyperopt,
|
'hyperopt': hyperopt,
|
||||||
'freqai': freqai,
|
'freqai': freqai,
|
||||||
|
'freqai_rl': freqai_rl,
|
||||||
'all': all_extra,
|
'all': all_extra,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
11
setup.sh
11
setup.sh
@ -78,14 +78,21 @@ function updateenv() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
REQUIREMENTS_FREQAI=""
|
REQUIREMENTS_FREQAI=""
|
||||||
|
REQUIREMENTS_FREQAI_RL=""
|
||||||
read -p "Do you want to install dependencies for freqai [y/N]? "
|
read -p "Do you want to install dependencies for freqai [y/N]? "
|
||||||
dev=$REPLY
|
dev=$REPLY
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]
|
if [[ $REPLY =~ ^[Yy]$ ]]
|
||||||
then
|
then
|
||||||
REQUIREMENTS_FREQAI="-r requirements-freqai.txt"
|
REQUIREMENTS_FREQAI="-r requirements-freqai.txt --use-pep517"
|
||||||
|
read -p "Do you also want dependencies for freqai-rl (~700mb additional space required) [y/N]? "
|
||||||
|
dev=$REPLY
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]
|
||||||
|
then
|
||||||
|
REQUIREMENTS_FREQAI="-r requirements-freqai-rl.txt"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} ${REQUIREMENTS_PLOT} ${REQUIREMENTS_FREQAI}
|
${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} ${REQUIREMENTS_PLOT} ${REQUIREMENTS_FREQAI} ${REQUIREMENTS_FREQAI_RL}
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "Failed installing dependencies"
|
echo "Failed installing dependencies"
|
||||||
exit 1
|
exit 1
|
||||||
|
@ -1271,7 +1271,7 @@ def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmpdir):
|
|||||||
assert csv_file.is_file()
|
assert csv_file.is_file()
|
||||||
line = csv_file.read_text()
|
line = csv_file.read_text()
|
||||||
assert ('Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in line
|
assert ('Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in line
|
||||||
or "Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,2 days 17:30:00,0.43662" in line)
|
or "Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,2 days 17:30:00,2,0,0.43662" in line)
|
||||||
csv_file.unlink()
|
csv_file.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
@ -2679,7 +2679,7 @@ def saved_hyperopt_results():
|
|||||||
'params_dict': {
|
'params_dict': {
|
||||||
'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501
|
'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501
|
||||||
'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501
|
'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501
|
||||||
'results_metrics': {'total_trades': 2, 'wins': 0, 'draws': 0, 'losses': 2, 'profit_mean': -0.01254995, 'profit_median': -0.012222, 'profit_total': -0.00125625, 'profit_total_abs': -2.50999, 'max_drawdown': 0.23, 'max_drawdown_abs': -0.00125625, 'holding_avg': timedelta(minutes=3930.0), 'stake_currency': 'BTC', 'strategy_name': 'SampleStrategy'}, # noqa: E501
|
'results_metrics': {'total_trades': 2, 'trade_count_long': 2, 'trade_count_short': 0, 'wins': 0, 'draws': 0, 'losses': 2, 'profit_mean': -0.01254995, 'profit_median': -0.012222, 'profit_total': -0.00125625, 'profit_total_abs': -2.50999, 'max_drawdown': 0.23, 'max_drawdown_abs': -0.00125625, 'holding_avg': timedelta(minutes=3930.0), 'stake_currency': 'BTC', 'strategy_name': 'SampleStrategy'}, # noqa: E501
|
||||||
'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501
|
'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501
|
||||||
'total_profit': -0.00125625,
|
'total_profit': -0.00125625,
|
||||||
'current_epoch': 1,
|
'current_epoch': 1,
|
||||||
@ -2696,7 +2696,7 @@ def saved_hyperopt_results():
|
|||||||
'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501
|
'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501
|
||||||
'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501
|
'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501
|
||||||
'stoploss': {'stoploss': -0.338070047333259}},
|
'stoploss': {'stoploss': -0.338070047333259}},
|
||||||
'results_metrics': {'total_trades': 1, 'wins': 0, 'draws': 0, 'losses': 1, 'profit_mean': 0.012357, 'profit_median': -0.012222, 'profit_total': 6.185e-05, 'profit_total_abs': 0.12357, 'max_drawdown': 0.23, 'max_drawdown_abs': -0.00125625, 'holding_avg': timedelta(minutes=1200.0)}, # noqa: E501
|
'results_metrics': {'total_trades': 1, 'trade_count_long': 1, 'trade_count_short': 0, 'wins': 0, 'draws': 0, 'losses': 1, 'profit_mean': 0.012357, 'profit_median': -0.012222, 'profit_total': 6.185e-05, 'profit_total_abs': 0.12357, 'max_drawdown': 0.23, 'max_drawdown_abs': -0.00125625, 'holding_avg': timedelta(minutes=1200.0)}, # noqa: E501
|
||||||
'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501
|
'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501
|
||||||
'total_profit': 6.185e-05,
|
'total_profit': 6.185e-05,
|
||||||
'current_epoch': 2,
|
'current_epoch': 2,
|
||||||
@ -2707,7 +2707,7 @@ def saved_hyperopt_results():
|
|||||||
'loss': 14.241196856510731,
|
'loss': 14.241196856510731,
|
||||||
'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, # noqa: E501
|
'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, # noqa: E501
|
||||||
'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, # noqa: E501
|
'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, # noqa: E501
|
||||||
'results_metrics': {'total_trades': 621, 'wins': 320, 'draws': 0, 'losses': 301, 'profit_mean': -0.043883302093397747, 'profit_median': -0.012222, 'profit_total': -0.13639474, 'profit_total_abs': -272.515306, 'max_drawdown': 0.25, 'max_drawdown_abs': -272.515306, 'holding_avg': timedelta(minutes=1691.207729468599)}, # noqa: E501
|
'results_metrics': {'total_trades': 621, 'trade_count_long': 621, 'trade_count_short': 0, 'wins': 320, 'draws': 0, 'losses': 301, 'profit_mean': -0.043883302093397747, 'profit_median': -0.012222, 'profit_total': -0.13639474, 'profit_total_abs': -272.515306, 'max_drawdown': 0.25, 'max_drawdown_abs': -272.515306, 'holding_avg': timedelta(minutes=1691.207729468599)}, # noqa: E501
|
||||||
'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 min.', # noqa: E501
|
'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 min.', # noqa: E501
|
||||||
'total_profit': -0.13639474,
|
'total_profit': -0.13639474,
|
||||||
'current_epoch': 3,
|
'current_epoch': 3,
|
||||||
@ -2718,14 +2718,14 @@ def saved_hyperopt_results():
|
|||||||
'loss': 100000,
|
'loss': 100000,
|
||||||
'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, # noqa: E501
|
'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, # noqa: E501
|
||||||
'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501
|
'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501
|
||||||
'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit': 0.0, 'holding_avg': timedelta()}, # noqa: E501
|
'results_metrics': {'total_trades': 0, 'trade_count_long': 0, 'trade_count_short': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit': 0.0, 'holding_avg': timedelta()}, # noqa: E501
|
||||||
'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501
|
'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501
|
||||||
'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_random': False, 'is_best': False # noqa: E501
|
'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_random': False, 'is_best': False # noqa: E501
|
||||||
}, {
|
}, {
|
||||||
'loss': 0.22195522184191518,
|
'loss': 0.22195522184191518,
|
||||||
'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501
|
'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501
|
||||||
'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, # noqa: E501
|
'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, # noqa: E501
|
||||||
'results_metrics': {'total_trades': 14, 'wins': 6, 'draws': 0, 'losses': 8, 'profit_mean': -0.003539515, 'profit_median': -0.012222, 'profit_total': -0.002480140000000001, 'profit_total_abs': -4.955321, 'max_drawdown': 0.34, 'max_drawdown_abs': -4.955321, 'holding_avg': timedelta(minutes=3402.8571428571427)}, # noqa: E501
|
'results_metrics': {'total_trades': 14, 'trade_count_long': 14, 'trade_count_short': 0, 'wins': 6, 'draws': 0, 'losses': 8, 'profit_mean': -0.003539515, 'profit_median': -0.012222, 'profit_total': -0.002480140000000001, 'profit_total_abs': -4.955321, 'max_drawdown': 0.34, 'max_drawdown_abs': -4.955321, 'holding_avg': timedelta(minutes=3402.8571428571427)}, # noqa: E501
|
||||||
'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 min.', # noqa: E501
|
'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 min.', # noqa: E501
|
||||||
'total_profit': -0.002480140000000001,
|
'total_profit': -0.002480140000000001,
|
||||||
'current_epoch': 5,
|
'current_epoch': 5,
|
||||||
@ -2736,7 +2736,7 @@ def saved_hyperopt_results():
|
|||||||
'loss': 0.545315889154162,
|
'loss': 0.545315889154162,
|
||||||
'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, # noqa: E501
|
'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, # noqa: E501
|
||||||
'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, # noqa: E501
|
'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, # noqa: E501
|
||||||
'results_metrics': {'total_trades': 39, 'wins': 20, 'draws': 0, 'losses': 19, 'profit_mean': -0.0021400679487179478, 'profit_median': -0.012222, 'profit_total': -0.0041773, 'profit_total_abs': -8.346264999999997, 'max_drawdown': 0.45, 'max_drawdown_abs': -4.955321, 'holding_avg': timedelta(minutes=636.9230769230769)}, # noqa: E501
|
'results_metrics': {'total_trades': 39, 'trade_count_long': 39, 'trade_count_short': 0, 'wins': 20, 'draws': 0, 'losses': 19, 'profit_mean': -0.0021400679487179478, 'profit_median': -0.012222, 'profit_total': -0.0041773, 'profit_total_abs': -8.346264999999997, 'max_drawdown': 0.45, 'max_drawdown_abs': -4.955321, 'holding_avg': timedelta(minutes=636.9230769230769)}, # noqa: E501
|
||||||
'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 min.', # noqa: E501
|
'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 min.', # noqa: E501
|
||||||
'total_profit': -0.0041773,
|
'total_profit': -0.0041773,
|
||||||
'current_epoch': 6,
|
'current_epoch': 6,
|
||||||
@ -2749,7 +2749,7 @@ def saved_hyperopt_results():
|
|||||||
'params_details': {
|
'params_details': {
|
||||||
'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501
|
'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501
|
||||||
'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501
|
'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501
|
||||||
'results_metrics': {'total_trades': 318, 'wins': 100, 'draws': 0, 'losses': 218, 'profit_mean': -0.0039833954716981146, 'profit_median': -0.012222, 'profit_total': -0.06339929, 'profit_total_abs': -126.67197600000004, 'max_drawdown': 0.50, 'max_drawdown_abs': -200.955321, 'holding_avg': timedelta(minutes=3140.377358490566)}, # noqa: E501
|
'results_metrics': {'total_trades': 318, 'trade_count_long': 318, 'trade_count_short': 0, 'wins': 100, 'draws': 0, 'losses': 218, 'profit_mean': -0.0039833954716981146, 'profit_median': -0.012222, 'profit_total': -0.06339929, 'profit_total_abs': -126.67197600000004, 'max_drawdown': 0.50, 'max_drawdown_abs': -200.955321, 'holding_avg': timedelta(minutes=3140.377358490566)}, # noqa: E501
|
||||||
'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501
|
'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501
|
||||||
'total_profit': -0.06339929,
|
'total_profit': -0.06339929,
|
||||||
'current_epoch': 7,
|
'current_epoch': 7,
|
||||||
@ -2760,7 +2760,7 @@ def saved_hyperopt_results():
|
|||||||
'loss': 20.0, # noqa: E501
|
'loss': 20.0, # noqa: E501
|
||||||
'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, # noqa: E501
|
'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, # noqa: E501
|
||||||
'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, # noqa: E501
|
'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, # noqa: E501
|
||||||
'results_metrics': {'total_trades': 1, 'wins': 0, 'draws': 1, 'losses': 0, 'profit_mean': 0.0, 'profit_median': 0.0, 'profit_total': 0.0, 'profit_total_abs': 0.0, 'max_drawdown': 0.0, 'max_drawdown_abs': 0.52, 'holding_avg': timedelta(minutes=5340.0)}, # noqa: E501
|
'results_metrics': {'total_trades': 1, 'trade_count_long': 1, 'trade_count_short': 0, 'wins': 0, 'draws': 1, 'losses': 0, 'profit_mean': 0.0, 'profit_median': 0.0, 'profit_total': 0.0, 'profit_total_abs': 0.0, 'max_drawdown': 0.0, 'max_drawdown_abs': 0.52, 'holding_avg': timedelta(minutes=5340.0)}, # noqa: E501
|
||||||
'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 min.', # noqa: E501
|
'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 min.', # noqa: E501
|
||||||
'total_profit': 0.0,
|
'total_profit': 0.0,
|
||||||
'current_epoch': 8,
|
'current_epoch': 8,
|
||||||
@ -2771,7 +2771,7 @@ def saved_hyperopt_results():
|
|||||||
'loss': 2.4731817780991223,
|
'loss': 2.4731817780991223,
|
||||||
'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, # noqa: E501
|
'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, # noqa: E501
|
||||||
'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, # noqa: E501
|
'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, # noqa: E501
|
||||||
'results_metrics': {'total_trades': 229, 'wins': 150, 'draws': 0, 'losses': 79, 'profit_mean': -0.0038433433624454144, 'profit_median': -0.012222, 'profit_total': -0.044050070000000004, 'profit_total_abs': -88.01256299999999, 'max_drawdown': 0.41, 'max_drawdown_abs': -150.955321, 'holding_avg': timedelta(minutes=6505.676855895196)}, # noqa: E501
|
'results_metrics': {'total_trades': 229, 'trade_count_long': 229, 'trade_count_short': 0, 'wins': 150, 'draws': 0, 'losses': 79, 'profit_mean': -0.0038433433624454144, 'profit_median': -0.012222, 'profit_total': -0.044050070000000004, 'profit_total_abs': -88.01256299999999, 'max_drawdown': 0.41, 'max_drawdown_abs': -150.955321, 'holding_avg': timedelta(minutes=6505.676855895196)}, # noqa: E501
|
||||||
'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 min.', # noqa: E501
|
'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 min.', # noqa: E501
|
||||||
'total_profit': -0.044050070000000004, # noqa: E501
|
'total_profit': -0.044050070000000004, # noqa: E501
|
||||||
'current_epoch': 9,
|
'current_epoch': 9,
|
||||||
@ -2782,7 +2782,7 @@ def saved_hyperopt_results():
|
|||||||
'loss': -0.2604606005845212, # noqa: E501
|
'loss': -0.2604606005845212, # noqa: E501
|
||||||
'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, # noqa: E501
|
'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, # noqa: E501
|
||||||
'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, # noqa: E501
|
'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, # noqa: E501
|
||||||
'results_metrics': {'total_trades': 4, 'wins': 0, 'draws': 0, 'losses': 4, 'profit_mean': 0.001080385, 'profit_median': -0.012222, 'profit_total': 0.00021629, 'profit_total_abs': 0.432154, 'max_drawdown': 0.13, 'max_drawdown_abs': -4.955321, 'holding_avg': timedelta(minutes=2850.0)}, # noqa: E501
|
'results_metrics': {'total_trades': 4, 'trade_count_long': 4, 'trade_count_short': 0, 'wins': 0, 'draws': 0, 'losses': 4, 'profit_mean': 0.001080385, 'profit_median': -0.012222, 'profit_total': 0.00021629, 'profit_total_abs': 0.432154, 'max_drawdown': 0.13, 'max_drawdown_abs': -4.955321, 'holding_avg': timedelta(minutes=2850.0)}, # noqa: E501
|
||||||
'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 min.', # noqa: E501
|
'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 min.', # noqa: E501
|
||||||
'total_profit': 0.00021629,
|
'total_profit': 0.00021629,
|
||||||
'current_epoch': 10,
|
'current_epoch': 10,
|
||||||
@ -2794,7 +2794,7 @@ def saved_hyperopt_results():
|
|||||||
'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, # noqa: E501
|
'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, # noqa: E501
|
||||||
'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, # noqa: E501
|
'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, # noqa: E501
|
||||||
# New Hyperopt mode!
|
# New Hyperopt mode!
|
||||||
'results_metrics': {'total_trades': 117, 'wins': 67, 'draws': 0, 'losses': 50, 'profit_mean': -0.012698609145299145, 'profit_median': -0.012222, 'profit_total': -0.07436117, 'profit_total_abs': -148.573727, 'max_drawdown': 0.52, 'max_drawdown_abs': -224.955321, 'holding_avg': timedelta(minutes=4282.5641025641025)}, # noqa: E501
|
'results_metrics': {'total_trades': 117, 'trade_count_long': 117, 'trade_count_short': 0, 'wins': 67, 'draws': 0, 'losses': 50, 'profit_mean': -0.012698609145299145, 'profit_median': -0.012222, 'profit_total': -0.07436117, 'profit_total_abs': -148.573727, 'max_drawdown': 0.52, 'max_drawdown_abs': -224.955321, 'holding_avg': timedelta(minutes=4282.5641025641025)}, # noqa: E501
|
||||||
'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 min.', # noqa: E501
|
'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 min.', # noqa: E501
|
||||||
'total_profit': -0.07436117,
|
'total_profit': -0.07436117,
|
||||||
'current_epoch': 11,
|
'current_epoch': 11,
|
||||||
@ -2805,7 +2805,7 @@ def saved_hyperopt_results():
|
|||||||
'loss': 100000,
|
'loss': 100000,
|
||||||
'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, # noqa: E501
|
'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, # noqa: E501
|
||||||
'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, # noqa: E501
|
'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, # noqa: E501
|
||||||
'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit_total_abs': 0.0, 'max_drawdown': 0.0, 'max_drawdown_abs': 0.0, 'holding_avg': timedelta()}, # noqa: E501
|
'results_metrics': {'total_trades': 0, 'trade_count_long': 0, 'trade_count_short': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit_total_abs': 0.0, 'max_drawdown': 0.0, 'max_drawdown_abs': 0.0, 'holding_avg': timedelta()}, # noqa: E501
|
||||||
'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501
|
'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501
|
||||||
'total_profit': 0,
|
'total_profit': 0,
|
||||||
'current_epoch': 12,
|
'current_epoch': 12,
|
||||||
|
@ -189,3 +189,10 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmp
|
|||||||
assert '0.5' in captured.out
|
assert '0.5' in captured.out
|
||||||
assert '1' in captured.out
|
assert '1' in captured.out
|
||||||
assert '2.5' in captured.out
|
assert '2.5' in captured.out
|
||||||
|
|
||||||
|
# test date filtering
|
||||||
|
args = get_args(base_args + ['--timerange', "20180129-20180130"])
|
||||||
|
start_analysis_entries_exits(args)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert 'enter_tag_long_a' in captured.out
|
||||||
|
assert 'enter_tag_long_b' not in captured.out
|
||||||
|
@ -1207,12 +1207,17 @@ def test_create_dry_run_order_fees(
|
|||||||
assert order1['fee']['rate'] == fee
|
assert order1['fee']['rate'] == fee
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("side,startprice,endprice", [
|
@pytest.mark.parametrize("side,price,filled", [
|
||||||
("buy", 25.563, 25.566),
|
# order_book_l2_usd spread:
|
||||||
("sell", 25.566, 25.563)
|
# best ask: 25.566
|
||||||
|
# best bid: 25.563
|
||||||
|
("buy", 25.563, False),
|
||||||
|
("buy", 25.566, True),
|
||||||
|
("sell", 25.566, False),
|
||||||
|
("sell", 25.563, True),
|
||||||
])
|
])
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice, endprice,
|
def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, filled,
|
||||||
exchange_name, order_book_l2_usd):
|
exchange_name, order_book_l2_usd):
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
@ -1226,7 +1231,7 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice,
|
|||||||
ordertype='limit',
|
ordertype='limit',
|
||||||
side=side,
|
side=side,
|
||||||
amount=1,
|
amount=1,
|
||||||
rate=startprice,
|
rate=price,
|
||||||
leverage=1.0
|
leverage=1.0
|
||||||
)
|
)
|
||||||
assert order_book_l2_usd.call_count == 1
|
assert order_book_l2_usd.call_count == 1
|
||||||
@ -1235,22 +1240,17 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice,
|
|||||||
assert order["side"] == side
|
assert order["side"] == side
|
||||||
assert order["type"] == "limit"
|
assert order["type"] == "limit"
|
||||||
assert order["symbol"] == "LTC/USDT"
|
assert order["symbol"] == "LTC/USDT"
|
||||||
|
assert order["average"] == price
|
||||||
|
assert order['status'] == 'open' if not filled else 'closed'
|
||||||
order_book_l2_usd.reset_mock()
|
order_book_l2_usd.reset_mock()
|
||||||
|
|
||||||
|
# fetch order again...
|
||||||
order_closed = exchange.fetch_dry_run_order(order['id'])
|
order_closed = exchange.fetch_dry_run_order(order['id'])
|
||||||
assert order_book_l2_usd.call_count == 1
|
assert order_book_l2_usd.call_count == (1 if not filled else 0)
|
||||||
assert order_closed['status'] == 'open'
|
assert order_closed['status'] == ('open' if not filled else 'closed')
|
||||||
assert not order['fee']
|
assert order_closed['filled'] == (0 if not filled else 1)
|
||||||
assert order_closed['filled'] == 0
|
|
||||||
|
|
||||||
order_book_l2_usd.reset_mock()
|
order_book_l2_usd.reset_mock()
|
||||||
order_closed['price'] = endprice
|
|
||||||
|
|
||||||
order_closed = exchange.fetch_dry_run_order(order['id'])
|
|
||||||
assert order_closed['status'] == 'closed'
|
|
||||||
assert order['fee']
|
|
||||||
assert order_closed['filled'] == 1
|
|
||||||
assert order_closed['filled'] == order_closed['amount']
|
|
||||||
|
|
||||||
# Empty orderbook test
|
# Empty orderbook test
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book',
|
mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book',
|
||||||
|
@ -27,10 +27,9 @@ def freqai_conf(default_conf, tmpdir):
|
|||||||
"timerange": "20180110-20180115",
|
"timerange": "20180110-20180115",
|
||||||
"freqai": {
|
"freqai": {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"startup_candles": 10000,
|
|
||||||
"purge_old_models": True,
|
"purge_old_models": True,
|
||||||
"train_period_days": 2,
|
"train_period_days": 2,
|
||||||
"backtest_period_days": 2,
|
"backtest_period_days": 10,
|
||||||
"live_retrain_hours": 0,
|
"live_retrain_hours": 0,
|
||||||
"expiration_hours": 1,
|
"expiration_hours": 1,
|
||||||
"identifier": "uniqe-id100",
|
"identifier": "uniqe-id100",
|
||||||
@ -58,6 +57,30 @@ def freqai_conf(default_conf, tmpdir):
|
|||||||
return freqaiconf
|
return freqaiconf
|
||||||
|
|
||||||
|
|
||||||
|
def make_rl_config(conf):
|
||||||
|
conf.update({"strategy": "freqai_rl_test_strat"})
|
||||||
|
conf["freqai"].update({"model_training_parameters": {
|
||||||
|
"learning_rate": 0.00025,
|
||||||
|
"gamma": 0.9,
|
||||||
|
"verbose": 1
|
||||||
|
}})
|
||||||
|
conf["freqai"]["rl_config"] = {
|
||||||
|
"train_cycles": 1,
|
||||||
|
"thread_count": 2,
|
||||||
|
"max_trade_duration_candles": 300,
|
||||||
|
"model_type": "PPO",
|
||||||
|
"policy_type": "MlpPolicy",
|
||||||
|
"max_training_drawdown_pct": 0.5,
|
||||||
|
"net_arch": [32, 32],
|
||||||
|
"model_reward_parameters": {
|
||||||
|
"rr": 1,
|
||||||
|
"profit_aim": 0.02,
|
||||||
|
"win_reward_factor": 2
|
||||||
|
}}
|
||||||
|
|
||||||
|
return conf
|
||||||
|
|
||||||
|
|
||||||
def get_patched_data_kitchen(mocker, freqaiconf):
|
def get_patched_data_kitchen(mocker, freqaiconf):
|
||||||
dk = FreqaiDataKitchen(freqaiconf)
|
dk = FreqaiDataKitchen(freqaiconf)
|
||||||
return dk
|
return dk
|
||||||
|
@ -65,6 +65,8 @@ def test_freqai_backtest_live_models_model_not_found(freqai_conf, mocker, testda
|
|||||||
mocker.patch('freqtrade.optimize.backtesting.history.load_data')
|
mocker.patch('freqtrade.optimize.backtesting.history.load_data')
|
||||||
mocker.patch('freqtrade.optimize.backtesting.history.get_timerange', return_value=(now, now))
|
mocker.patch('freqtrade.optimize.backtesting.history.get_timerange', return_value=(now, now))
|
||||||
freqai_conf["timerange"] = ""
|
freqai_conf["timerange"] = ""
|
||||||
|
freqai_conf.get("freqai", {}).update({"backtest_using_historic_predictions": False})
|
||||||
|
|
||||||
patched_configuration_load_config_file(mocker, freqai_conf)
|
patched_configuration_load_config_file(mocker, freqai_conf)
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
@ -79,7 +81,7 @@ def test_freqai_backtest_live_models_model_not_found(freqai_conf, mocker, testda
|
|||||||
bt_config = setup_optimize_configuration(args, RunMode.BACKTEST)
|
bt_config = setup_optimize_configuration(args, RunMode.BACKTEST)
|
||||||
|
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r".* Saved models are required to run backtest .*"):
|
match=r".* Historic predictions data is required to run backtest .*"):
|
||||||
Backtesting(bt_config)
|
Backtesting(bt_config)
|
||||||
|
|
||||||
Backtesting.cleanup()
|
Backtesting.cleanup()
|
||||||
|
@ -2,8 +2,11 @@
|
|||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
from tests.conftest import get_patched_exchange
|
from tests.conftest import get_patched_exchange
|
||||||
from tests.freqai.conftest import get_patched_freqai_strategy
|
from tests.freqai.conftest import get_patched_freqai_strategy
|
||||||
@ -93,3 +96,37 @@ def test_use_strategy_to_populate_indicators(mocker, freqai_conf):
|
|||||||
|
|
||||||
assert len(df.columns) == 33
|
assert len(df.columns) == 33
|
||||||
shutil.rmtree(Path(freqai.dk.full_path))
|
shutil.rmtree(Path(freqai.dk.full_path))
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_timerange_from_live_historic_predictions(mocker, freqai_conf):
|
||||||
|
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||||
|
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||||
|
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||||
|
freqai = strategy.freqai
|
||||||
|
freqai.live = True
|
||||||
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
timerange = TimeRange.parse_timerange("20180126-20180130")
|
||||||
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
|
sub_timerange = TimeRange.parse_timerange("20180128-20180130")
|
||||||
|
_, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "ADA/BTC", freqai.dk)
|
||||||
|
base_df["5m"]["date_pred"] = base_df["5m"]["date"]
|
||||||
|
freqai.dd.historic_predictions = {}
|
||||||
|
freqai.dd.historic_predictions["ADA/USDT"] = base_df["5m"]
|
||||||
|
freqai.dd.save_historic_predictions_to_disk()
|
||||||
|
freqai.dd.save_global_metadata_to_disk({"start_dry_live_date": 1516406400})
|
||||||
|
|
||||||
|
timerange = freqai.dd.get_timerange_from_live_historic_predictions()
|
||||||
|
assert timerange.startts == 1516406400
|
||||||
|
assert timerange.stopts == 1517356500
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_timerange_from_backtesting_live_df_pred_not_found(mocker, freqai_conf):
|
||||||
|
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||||
|
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||||
|
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||||
|
freqai = strategy.freqai
|
||||||
|
with pytest.raises(
|
||||||
|
OperationalException,
|
||||||
|
match=r'Historic predictions not found.*'
|
||||||
|
):
|
||||||
|
freqai.dd.get_timerange_from_live_historic_predictions()
|
||||||
|
@ -9,7 +9,6 @@ from freqtrade.configuration import TimeRange
|
|||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
from freqtrade.freqai.utils import get_timerange_backtest_live_models
|
|
||||||
from tests.conftest import get_patched_exchange, log_has_re
|
from tests.conftest import get_patched_exchange, log_has_re
|
||||||
from tests.freqai.conftest import (get_patched_data_kitchen, get_patched_freqai_strategy,
|
from tests.freqai.conftest import (get_patched_data_kitchen, get_patched_freqai_strategy,
|
||||||
make_data_dictionary, make_unfiltered_dataframe)
|
make_data_dictionary, make_unfiltered_dataframe)
|
||||||
@ -166,71 +165,6 @@ def test_make_train_test_datasets(mocker, freqai_conf):
|
|||||||
assert len(data_dictionary['train_features'].index) == 1916
|
assert len(data_dictionary['train_features'].index) == 1916
|
||||||
|
|
||||||
|
|
||||||
def test_get_pairs_timestamp_validation(mocker, freqai_conf):
|
|
||||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
|
||||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
|
||||||
strategy.dp = DataProvider(freqai_conf, exchange)
|
|
||||||
strategy.freqai_info = freqai_conf.get("freqai", {})
|
|
||||||
freqai = strategy.freqai
|
|
||||||
freqai.live = True
|
|
||||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
|
||||||
freqai_conf['freqai'].update({"identifier": "invalid_id"})
|
|
||||||
model_path = freqai.dk.get_full_models_path(freqai_conf)
|
|
||||||
with pytest.raises(
|
|
||||||
OperationalException,
|
|
||||||
match=r'.*required to run backtest with the freqai-backtest-live-models.*'
|
|
||||||
):
|
|
||||||
freqai.dk.get_assets_timestamps_training_from_ready_models(model_path)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('model', [
|
|
||||||
'LightGBMRegressor'
|
|
||||||
])
|
|
||||||
def test_get_timerange_from_ready_models(mocker, freqai_conf, model):
|
|
||||||
freqai_conf.update({"freqaimodel": model})
|
|
||||||
freqai_conf.update({"timerange": "20180110-20180130"})
|
|
||||||
freqai_conf.update({"strategy": "freqai_test_strat"})
|
|
||||||
|
|
||||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
|
||||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
|
||||||
strategy.dp = DataProvider(freqai_conf, exchange)
|
|
||||||
strategy.freqai_info = freqai_conf.get("freqai", {})
|
|
||||||
freqai = strategy.freqai
|
|
||||||
freqai.live = True
|
|
||||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
|
||||||
timerange = TimeRange.parse_timerange("20180101-20180130")
|
|
||||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
|
||||||
|
|
||||||
freqai.dd.pair_dict = MagicMock()
|
|
||||||
|
|
||||||
data_load_timerange = TimeRange.parse_timerange("20180101-20180130")
|
|
||||||
|
|
||||||
# 1516233600 (2018-01-18 00:00) - Start Training 1
|
|
||||||
# 1516406400 (2018-01-20 00:00) - End Training 1 (Backtest slice 1)
|
|
||||||
# 1516579200 (2018-01-22 00:00) - End Training 2 (Backtest slice 2)
|
|
||||||
# 1516838400 (2018-01-25 00:00) - End Timerange
|
|
||||||
|
|
||||||
new_timerange = TimeRange("date", "date", 1516233600, 1516406400)
|
|
||||||
freqai.extract_data_and_train_model(
|
|
||||||
new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange)
|
|
||||||
|
|
||||||
new_timerange = TimeRange("date", "date", 1516406400, 1516579200)
|
|
||||||
freqai.extract_data_and_train_model(
|
|
||||||
new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange)
|
|
||||||
|
|
||||||
model_path = freqai.dk.get_full_models_path(freqai_conf)
|
|
||||||
(backtesting_timerange,
|
|
||||||
pairs_end_dates) = freqai.dk.get_timerange_and_assets_end_dates_from_ready_models(
|
|
||||||
models_path=model_path)
|
|
||||||
|
|
||||||
assert len(pairs_end_dates["ADA"]) == 2
|
|
||||||
assert backtesting_timerange.startts == 1516406400
|
|
||||||
assert backtesting_timerange.stopts == 1516838400
|
|
||||||
|
|
||||||
backtesting_string_timerange = get_timerange_backtest_live_models(freqai_conf)
|
|
||||||
assert backtesting_string_timerange == '20180120-20180125'
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('model', [
|
@pytest.mark.parametrize('model', [
|
||||||
'LightGBMRegressor'
|
'LightGBMRegressor'
|
||||||
])
|
])
|
||||||
|
@ -13,8 +13,8 @@ from freqtrade.freqai.utils import download_all_data_for_training, get_required_
|
|||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||||
from tests.conftest import get_patched_exchange, log_has_re
|
from tests.conftest import create_mock_trades, get_patched_exchange, log_has_re
|
||||||
from tests.freqai.conftest import get_patched_freqai_strategy
|
from tests.freqai.conftest import get_patched_freqai_strategy, make_rl_config
|
||||||
|
|
||||||
|
|
||||||
def is_arm() -> bool:
|
def is_arm() -> bool:
|
||||||
@ -32,11 +32,17 @@ def is_mac() -> bool:
|
|||||||
('XGBoostRegressor', False, True, False),
|
('XGBoostRegressor', False, True, False),
|
||||||
('XGBoostRFRegressor', False, False, False),
|
('XGBoostRFRegressor', False, False, False),
|
||||||
('CatboostRegressor', False, False, False),
|
('CatboostRegressor', False, False, False),
|
||||||
|
('ReinforcementLearner', False, True, False),
|
||||||
|
('ReinforcementLearner_multiproc', False, False, False),
|
||||||
|
('ReinforcementLearner_test_4ac', False, False, False)
|
||||||
])
|
])
|
||||||
def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, dbscan, float32):
|
def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, dbscan, float32):
|
||||||
if is_arm() and model == 'CatboostRegressor':
|
if is_arm() and model == 'CatboostRegressor':
|
||||||
pytest.skip("CatBoost is not supported on ARM")
|
pytest.skip("CatBoost is not supported on ARM")
|
||||||
|
|
||||||
|
if is_mac() and 'Reinforcement' in model:
|
||||||
|
pytest.skip("Reinforcement learning module not available on intel based Mac OS")
|
||||||
|
|
||||||
model_save_ext = 'joblib'
|
model_save_ext = 'joblib'
|
||||||
freqai_conf.update({"freqaimodel": model})
|
freqai_conf.update({"freqaimodel": model})
|
||||||
freqai_conf.update({"timerange": "20180110-20180130"})
|
freqai_conf.update({"timerange": "20180110-20180130"})
|
||||||
@ -45,6 +51,26 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
|
|||||||
freqai_conf['freqai']['feature_parameters'].update({"use_DBSCAN_to_remove_outliers": dbscan})
|
freqai_conf['freqai']['feature_parameters'].update({"use_DBSCAN_to_remove_outliers": dbscan})
|
||||||
freqai_conf.update({"reduce_df_footprint": float32})
|
freqai_conf.update({"reduce_df_footprint": float32})
|
||||||
|
|
||||||
|
if 'ReinforcementLearner' in model:
|
||||||
|
model_save_ext = 'zip'
|
||||||
|
freqai_conf = make_rl_config(freqai_conf)
|
||||||
|
# test the RL guardrails
|
||||||
|
freqai_conf['freqai']['feature_parameters'].update({"use_SVM_to_remove_outliers": True})
|
||||||
|
freqai_conf['freqai']['data_split_parameters'].update({'shuffle': True})
|
||||||
|
|
||||||
|
if 'test_4ac' in model:
|
||||||
|
freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models")
|
||||||
|
|
||||||
|
if 'ReinforcementLearner' in model:
|
||||||
|
model_save_ext = 'zip'
|
||||||
|
freqai_conf = make_rl_config(freqai_conf)
|
||||||
|
# test the RL guardrails
|
||||||
|
freqai_conf['freqai']['feature_parameters'].update({"use_SVM_to_remove_outliers": True})
|
||||||
|
freqai_conf['freqai']['data_split_parameters'].update({'shuffle': True})
|
||||||
|
|
||||||
|
if 'test_4ac' in model:
|
||||||
|
freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models")
|
||||||
|
|
||||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
strategy = get_patched_freqai_strategy(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)
|
||||||
@ -52,6 +78,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
|
|||||||
freqai = strategy.freqai
|
freqai = strategy.freqai
|
||||||
freqai.live = True
|
freqai.live = True
|
||||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
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)
|
||||||
|
|
||||||
@ -165,25 +192,35 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model):
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"model, num_files, strat",
|
"model, num_files, strat",
|
||||||
[
|
[
|
||||||
("LightGBMRegressor", 6, "freqai_test_strat"),
|
("LightGBMRegressor", 2, "freqai_test_strat"),
|
||||||
("XGBoostRegressor", 6, "freqai_test_strat"),
|
("XGBoostRegressor", 2, "freqai_test_strat"),
|
||||||
("CatboostRegressor", 6, "freqai_test_strat"),
|
("CatboostRegressor", 2, "freqai_test_strat"),
|
||||||
("XGBoostClassifier", 6, "freqai_test_classifier"),
|
("ReinforcementLearner", 3, "freqai_rl_test_strat"),
|
||||||
("LightGBMClassifier", 6, "freqai_test_classifier"),
|
("XGBoostClassifier", 2, "freqai_test_classifier"),
|
||||||
("CatboostClassifier", 6, "freqai_test_classifier")
|
("LightGBMClassifier", 2, "freqai_test_classifier"),
|
||||||
|
("CatboostClassifier", 2, "freqai_test_classifier")
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog):
|
def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog):
|
||||||
freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
|
freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
|
||||||
freqai_conf['runmode'] = RunMode.BACKTEST
|
freqai_conf['runmode'] = RunMode.BACKTEST
|
||||||
Trade.use_db = False
|
|
||||||
if is_arm() and "Catboost" in model:
|
if is_arm() and "Catboost" in model:
|
||||||
pytest.skip("CatBoost is not supported on ARM")
|
pytest.skip("CatBoost is not supported on ARM")
|
||||||
|
|
||||||
|
if is_mac() and 'Reinforcement' in model:
|
||||||
|
pytest.skip("Reinforcement learning module not available on intel based Mac OS")
|
||||||
|
Trade.use_db = False
|
||||||
|
|
||||||
freqai_conf.update({"freqaimodel": model})
|
freqai_conf.update({"freqaimodel": model})
|
||||||
freqai_conf.update({"timerange": "20180120-20180130"})
|
freqai_conf.update({"timerange": "20180120-20180130"})
|
||||||
freqai_conf.update({"strategy": strat})
|
freqai_conf.update({"strategy": strat})
|
||||||
|
|
||||||
|
if 'ReinforcementLearner' in model:
|
||||||
|
freqai_conf = make_rl_config(freqai_conf)
|
||||||
|
|
||||||
|
if 'test_4ac' in model:
|
||||||
|
freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models")
|
||||||
|
|
||||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
strategy = get_patched_freqai_strategy(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)
|
||||||
@ -207,6 +244,7 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog)
|
|||||||
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
|
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
|
||||||
|
|
||||||
assert len(model_folders) == num_files
|
assert len(model_folders) == num_files
|
||||||
|
Trade.use_db = True
|
||||||
assert log_has_re(
|
assert log_has_re(
|
||||||
"Removed features ",
|
"Removed features ",
|
||||||
caplog,
|
caplog,
|
||||||
@ -263,11 +301,13 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
|
|||||||
|
|
||||||
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")
|
||||||
|
|
||||||
metadata = {"pair": "ADA/BTC"}
|
pair = "ADA/BTC"
|
||||||
|
metadata = {"pair": pair}
|
||||||
|
freqai.dk.pair = pair
|
||||||
freqai.start_backtesting(df, metadata, freqai.dk)
|
freqai.start_backtesting(df, metadata, freqai.dk)
|
||||||
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
|
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
|
||||||
|
|
||||||
assert len(model_folders) == 6
|
assert len(model_folders) == 2
|
||||||
|
|
||||||
# without deleting the existing folder structure, re-run
|
# without deleting the existing folder structure, re-run
|
||||||
|
|
||||||
@ -286,6 +326,9 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
|
|||||||
|
|
||||||
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")
|
||||||
|
|
||||||
|
pair = "ADA/BTC"
|
||||||
|
metadata = {"pair": pair}
|
||||||
|
freqai.dk.pair = pair
|
||||||
freqai.start_backtesting(df, metadata, freqai.dk)
|
freqai.start_backtesting(df, metadata, freqai.dk)
|
||||||
|
|
||||||
assert log_has_re(
|
assert log_has_re(
|
||||||
@ -293,13 +336,43 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
|
|||||||
caplog,
|
caplog,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
pair = "ETH/BTC"
|
||||||
|
metadata = {"pair": pair}
|
||||||
|
freqai.dk.pair = pair
|
||||||
|
freqai.start_backtesting(df, metadata, freqai.dk)
|
||||||
|
|
||||||
path = (freqai.dd.full_path / freqai.dk.backtest_predictions_folder)
|
path = (freqai.dd.full_path / freqai.dk.backtest_predictions_folder)
|
||||||
prediction_files = [x for x in path.iterdir() if x.is_file()]
|
prediction_files = [x for x in path.iterdir() if x.is_file()]
|
||||||
assert len(prediction_files) == 5
|
assert len(prediction_files) == 2
|
||||||
|
|
||||||
shutil.rmtree(Path(freqai.dk.full_path))
|
shutil.rmtree(Path(freqai.dk.full_path))
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtesting_fit_live_predictions(mocker, freqai_conf, caplog):
|
||||||
|
freqai_conf.get("freqai", {}).update({"fit_live_predictions_candles": 10})
|
||||||
|
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||||
|
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||||
|
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||||
|
strategy.freqai_info = freqai_conf.get("freqai", {})
|
||||||
|
freqai = strategy.freqai
|
||||||
|
freqai.live = False
|
||||||
|
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||||
|
timerange = TimeRange.parse_timerange("20180128-20180130")
|
||||||
|
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||||
|
sub_timerange = TimeRange.parse_timerange("20180129-20180130")
|
||||||
|
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")
|
||||||
|
freqai.dk.pair = "ADA/BTC"
|
||||||
|
freqai.dk.full_df = df.fillna(0)
|
||||||
|
freqai.dk.full_df
|
||||||
|
assert "&-s_close_mean" not in freqai.dk.full_df.columns
|
||||||
|
assert "&-s_close_std" not in freqai.dk.full_df.columns
|
||||||
|
freqai.backtesting_fit_live_predictions(freqai.dk)
|
||||||
|
assert "&-s_close_mean" in freqai.dk.full_df.columns
|
||||||
|
assert "&-s_close_std" in freqai.dk.full_df.columns
|
||||||
|
shutil.rmtree(Path(freqai.dk.full_path))
|
||||||
|
|
||||||
|
|
||||||
def test_follow_mode(mocker, freqai_conf):
|
def test_follow_mode(mocker, freqai_conf):
|
||||||
freqai_conf.update({"timerange": "20180110-20180130"})
|
freqai_conf.update({"timerange": "20180110-20180130"})
|
||||||
|
|
||||||
@ -473,3 +546,43 @@ def test_download_all_data_for_training(mocker, freqai_conf, caplog, tmpdir):
|
|||||||
"Downloading",
|
"Downloading",
|
||||||
caplog,
|
caplog,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
@pytest.mark.parametrize('dp_exists', [(False), (True)])
|
||||||
|
def test_get_state_info(mocker, freqai_conf, dp_exists, caplog, tickers):
|
||||||
|
|
||||||
|
if is_mac():
|
||||||
|
pytest.skip("Reinforcement learning module not available on intel based Mac OS")
|
||||||
|
|
||||||
|
freqai_conf.update({"freqaimodel": "ReinforcementLearner"})
|
||||||
|
freqai_conf.update({"timerange": "20180110-20180130"})
|
||||||
|
freqai_conf.update({"strategy": "freqai_rl_test_strat"})
|
||||||
|
freqai_conf = make_rl_config(freqai_conf)
|
||||||
|
freqai_conf['entry_pricing']['price_side'] = 'same'
|
||||||
|
freqai_conf['exit_pricing']['price_side'] = 'same'
|
||||||
|
|
||||||
|
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||||
|
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||||
|
ticker_mock = MagicMock(return_value=tickers()['ETH/BTC'])
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock)
|
||||||
|
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||||
|
|
||||||
|
if not dp_exists:
|
||||||
|
strategy.dp._exchange = None
|
||||||
|
|
||||||
|
strategy.freqai_info = freqai_conf.get("freqai", {})
|
||||||
|
freqai = strategy.freqai
|
||||||
|
freqai.data_provider = strategy.dp
|
||||||
|
freqai.live = True
|
||||||
|
|
||||||
|
Trade.use_db = True
|
||||||
|
create_mock_trades(MagicMock(return_value=0.0025), False, True)
|
||||||
|
freqai.get_state_info("ADA/BTC")
|
||||||
|
freqai.get_state_info("ETH/BTC")
|
||||||
|
|
||||||
|
if not dp_exists:
|
||||||
|
assert log_has_re(
|
||||||
|
"No exchange available",
|
||||||
|
caplog,
|
||||||
|
)
|
||||||
|
66
tests/freqai/test_models/ReinforcementLearner_test_4ac.py
Normal file
66
tests/freqai/test_models/ReinforcementLearner_test_4ac.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner
|
||||||
|
from freqtrade.freqai.RL.Base4ActionRLEnv import Actions, Base4ActionRLEnv, Positions
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ReinforcementLearner_test_4ac(ReinforcementLearner):
|
||||||
|
"""
|
||||||
|
User created Reinforcement Learning Model prediction model.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class MyRLEnv(Base4ActionRLEnv):
|
||||||
|
"""
|
||||||
|
User can override any function in BaseRLEnv and gym.Env. Here the user
|
||||||
|
sets a custom reward based on profit and trade duration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def calculate_reward(self, action: int) -> float:
|
||||||
|
|
||||||
|
# first, penalize if the action is not valid
|
||||||
|
if not self._is_valid(action):
|
||||||
|
return -2
|
||||||
|
|
||||||
|
pnl = self.get_unrealized_profit()
|
||||||
|
rew = np.sign(pnl) * (pnl + 1)
|
||||||
|
factor = 100.
|
||||||
|
|
||||||
|
# reward agent for entering trades
|
||||||
|
if (action in (Actions.Long_enter.value, Actions.Short_enter.value)
|
||||||
|
and self._position == Positions.Neutral):
|
||||||
|
return 25
|
||||||
|
# discourage agent from not entering trades
|
||||||
|
if action == Actions.Neutral.value and self._position == Positions.Neutral:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
|
||||||
|
trade_duration = self._current_tick - self._last_trade_tick # type: ignore
|
||||||
|
|
||||||
|
if trade_duration <= max_trade_duration:
|
||||||
|
factor *= 1.5
|
||||||
|
elif trade_duration > max_trade_duration:
|
||||||
|
factor *= 0.5
|
||||||
|
|
||||||
|
# discourage sitting in position
|
||||||
|
if (self._position in (Positions.Short, Positions.Long) and
|
||||||
|
action == Actions.Neutral.value):
|
||||||
|
return -1 * trade_duration / max_trade_duration
|
||||||
|
|
||||||
|
# close long
|
||||||
|
if action == Actions.Exit.value and self._position == Positions.Long:
|
||||||
|
if pnl > self.profit_aim * self.rr:
|
||||||
|
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||||
|
return float(rew * factor)
|
||||||
|
|
||||||
|
# close short
|
||||||
|
if action == Actions.Exit.value and self._position == Positions.Short:
|
||||||
|
if pnl > self.profit_aim * self.rr:
|
||||||
|
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||||
|
return float(rew * factor)
|
||||||
|
|
||||||
|
return 0.
|
@ -663,30 +663,9 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
|
|||||||
'', # Exit Signal Name
|
'', # Exit Signal Name
|
||||||
|
|
||||||
]
|
]
|
||||||
row_detail = pd.DataFrame(
|
|
||||||
[
|
|
||||||
[
|
|
||||||
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0, tzinfo=timezone.utc),
|
|
||||||
200, 200.1, 197, 199, 1, 0, 0, 0, '', '', '',
|
|
||||||
], [
|
|
||||||
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=1, tzinfo=timezone.utc),
|
|
||||||
199, 199.7, 199, 199.5, 0, 0, 0, 0, '', '', '',
|
|
||||||
], [
|
|
||||||
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=2, tzinfo=timezone.utc),
|
|
||||||
199.5, 200.8, 199, 200.9, 0, 0, 0, 0, '', '', '',
|
|
||||||
], [
|
|
||||||
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=3, tzinfo=timezone.utc),
|
|
||||||
200.5, 210.5, 193, 210.5, 0, 0, 0, 0, '', '', '', # ROI sell (?)
|
|
||||||
], [
|
|
||||||
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=4, tzinfo=timezone.utc),
|
|
||||||
200, 200.1, 193, 199, 0, 0, 0, 0, '', '', '',
|
|
||||||
],
|
|
||||||
], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
|
||||||
'enter_short', 'exit_short', 'long_tag', 'short_tag', 'exit_tag']
|
|
||||||
)
|
|
||||||
|
|
||||||
# No data available.
|
# No data available.
|
||||||
res = backtesting._get_exit_trade_entry(trade, row_sell)
|
res = backtesting._get_exit_trade_entry(trade, row_sell, True)
|
||||||
assert res is not None
|
assert res is not None
|
||||||
assert res.exit_reason == ExitType.ROI.value
|
assert res.exit_reason == ExitType.ROI.value
|
||||||
assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc)
|
assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc)
|
||||||
@ -699,20 +678,9 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
|
|||||||
[], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
[], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
||||||
'enter_short', 'exit_short', 'long_tag', 'short_tag', 'exit_tag'])
|
'enter_short', 'exit_short', 'long_tag', 'short_tag', 'exit_tag'])
|
||||||
|
|
||||||
res = backtesting._get_exit_trade_entry(trade, row)
|
res = backtesting._get_exit_trade_entry(trade, row, True)
|
||||||
assert res is None
|
assert res is None
|
||||||
|
|
||||||
# Assign backtest-detail data
|
|
||||||
backtesting.detail_data[pair] = row_detail
|
|
||||||
|
|
||||||
res = backtesting._get_exit_trade_entry(trade, row_sell)
|
|
||||||
assert res is not None
|
|
||||||
assert res.exit_reason == ExitType.ROI.value
|
|
||||||
# Sell at minute 3 (not available above!)
|
|
||||||
assert res.close_date_utc == datetime(2020, 1, 1, 5, 3, tzinfo=timezone.utc)
|
|
||||||
sell_order = res.select_order('sell', True)
|
|
||||||
assert sell_order is not None
|
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
||||||
default_conf['use_exit_signal'] = False
|
default_conf['use_exit_signal'] = False
|
||||||
@ -788,17 +756,98 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
|||||||
for _, t in results.iterrows():
|
for _, t in results.iterrows():
|
||||||
assert len(t['orders']) == 2
|
assert len(t['orders']) == 2
|
||||||
ln = data_pair.loc[data_pair["date"] == t["open_date"]]
|
ln = data_pair.loc[data_pair["date"] == t["open_date"]]
|
||||||
# Check open trade rate alignes to open rate
|
# Check open trade rate aligns to open rate
|
||||||
assert not ln.empty
|
assert not ln.empty
|
||||||
assert round(ln.iloc[0]["open"], 6) == round(t["open_rate"], 6)
|
assert round(ln.iloc[0]["open"], 6) == round(t["open_rate"], 6)
|
||||||
# check close trade rate alignes to close rate or is between high and low
|
# check close trade rate aligns to close rate or is between high and low
|
||||||
ln1 = data_pair.loc[data_pair["date"] == t["close_date"]]
|
ln1 = data_pair.loc[data_pair["date"] == t["close_date"]]
|
||||||
assert not ln1.empty
|
|
||||||
assert (round(ln1.iloc[0]["open"], 6) == round(t["close_rate"], 6) or
|
assert (round(ln1.iloc[0]["open"], 6) == round(t["close_rate"], 6) or
|
||||||
round(ln1.iloc[0]["low"], 6) < round(
|
round(ln1.iloc[0]["low"], 6) < round(
|
||||||
t["close_rate"], 6) < round(ln1.iloc[0]["high"], 6))
|
t["close_rate"], 6) < round(ln1.iloc[0]["high"], 6))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('use_detail', [True, False])
|
||||||
|
def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None:
|
||||||
|
default_conf_usdt['use_exit_signal'] = False
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||||
|
if use_detail:
|
||||||
|
default_conf_usdt['timeframe_detail'] = '1m'
|
||||||
|
patch_exchange(mocker)
|
||||||
|
|
||||||
|
def advise_entry(df, *args, **kwargs):
|
||||||
|
# Mock function to force several entries
|
||||||
|
df.loc[(df['rsi'] < 40), 'enter_long'] = 1
|
||||||
|
return df
|
||||||
|
|
||||||
|
def custom_entry_price(proposed_rate, **kwargs):
|
||||||
|
return proposed_rate * 0.997
|
||||||
|
|
||||||
|
backtesting = Backtesting(default_conf_usdt)
|
||||||
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
|
backtesting.strategy.populate_entry_trend = advise_entry
|
||||||
|
backtesting.strategy.custom_entry_price = custom_entry_price
|
||||||
|
pair = 'XRP/ETH'
|
||||||
|
# Pick a timerange adapted to the pair we use to test
|
||||||
|
timerange = TimeRange.parse_timerange('20191010-20191013')
|
||||||
|
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['XRP/ETH'],
|
||||||
|
timerange=timerange)
|
||||||
|
if use_detail:
|
||||||
|
data_1m = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['XRP/ETH'],
|
||||||
|
timerange=timerange)
|
||||||
|
backtesting.detail_data = data_1m
|
||||||
|
processed = backtesting.strategy.advise_all_indicators(data)
|
||||||
|
min_date, max_date = get_timerange(processed)
|
||||||
|
|
||||||
|
result = backtesting.backtest(
|
||||||
|
processed=deepcopy(processed),
|
||||||
|
start_date=min_date,
|
||||||
|
end_date=max_date,
|
||||||
|
max_open_trades=10,
|
||||||
|
)
|
||||||
|
results = result['results']
|
||||||
|
assert not results.empty
|
||||||
|
# Timeout settings from default_conf = entry: 10, exit: 30
|
||||||
|
assert len(results) == (2 if use_detail else 3)
|
||||||
|
|
||||||
|
assert 'orders' in results.columns
|
||||||
|
data_pair = processed[pair]
|
||||||
|
|
||||||
|
data_1m_pair = data_1m[pair] if use_detail else pd.DataFrame()
|
||||||
|
late_entry = 0
|
||||||
|
for _, t in results.iterrows():
|
||||||
|
assert len(t['orders']) == 2
|
||||||
|
|
||||||
|
entryo = t['orders'][0]
|
||||||
|
entry_ts = datetime.fromtimestamp(entryo['order_filled_timestamp'] // 1000, tz=timezone.utc)
|
||||||
|
if entry_ts > t['open_date']:
|
||||||
|
late_entry += 1
|
||||||
|
|
||||||
|
# Get "entry fill" candle
|
||||||
|
ln = (data_1m_pair.loc[data_1m_pair["date"] == entry_ts]
|
||||||
|
if use_detail else data_pair.loc[data_pair["date"] == entry_ts])
|
||||||
|
# Check open trade rate aligns to open rate
|
||||||
|
assert not ln.empty
|
||||||
|
|
||||||
|
# assert round(ln.iloc[0]["open"], 6) == round(t["open_rate"], 6)
|
||||||
|
assert round(ln.iloc[0]["low"], 6) <= round(
|
||||||
|
t["open_rate"], 6) <= round(ln.iloc[0]["high"], 6)
|
||||||
|
# check close trade rate aligns to close rate or is between high and low
|
||||||
|
ln1 = data_pair.loc[data_pair["date"] == t["close_date"]]
|
||||||
|
if use_detail:
|
||||||
|
ln1_1m = data_1m_pair.loc[data_1m_pair["date"] == t["close_date"]]
|
||||||
|
assert not ln1.empty or not ln1_1m.empty
|
||||||
|
else:
|
||||||
|
assert not ln1.empty
|
||||||
|
ln2 = ln1_1m if ln1.empty else ln1
|
||||||
|
|
||||||
|
assert (round(ln2.iloc[0]["low"], 6) <= round(
|
||||||
|
t["close_rate"], 6) <= round(ln2.iloc[0]["high"], 6))
|
||||||
|
|
||||||
|
assert late_entry > 0
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir) -> None:
|
def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir) -> None:
|
||||||
# This strategy intentionally places unfillable orders.
|
# This strategy intentionally places unfillable orders.
|
||||||
default_conf['strategy'] = 'StrategyTestV3CustomEntryPrice'
|
default_conf['strategy'] = 'StrategyTestV3CustomEntryPrice'
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments
|
# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from unittest.mock import ANY, MagicMock, PropertyMock
|
from unittest.mock import ANY, MagicMock, PropertyMock
|
||||||
|
|
||||||
@ -28,113 +29,7 @@ def prec_satoshi(a, b) -> float:
|
|||||||
|
|
||||||
# Unit tests
|
# Unit tests
|
||||||
def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
gen_response = {
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=ticker,
|
|
||||||
get_fee=fee,
|
|
||||||
_is_dry_limit_order_filled=MagicMock(side_effect=[False, True]),
|
|
||||||
)
|
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
|
||||||
patch_get_signal(freqtradebot)
|
|
||||||
rpc = RPC(freqtradebot)
|
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
|
||||||
with pytest.raises(RPCException, match=r'.*no active trade*'):
|
|
||||||
rpc._rpc_trade_status()
|
|
||||||
|
|
||||||
freqtradebot.enter_positions()
|
|
||||||
|
|
||||||
# Open order...
|
|
||||||
results = rpc._rpc_trade_status()
|
|
||||||
assert results[0] == {
|
|
||||||
'trade_id': 1,
|
|
||||||
'pair': 'ETH/BTC',
|
|
||||||
'base_currency': 'ETH',
|
|
||||||
'quote_currency': 'BTC',
|
|
||||||
'open_date': ANY,
|
|
||||||
'open_timestamp': ANY,
|
|
||||||
'is_open': ANY,
|
|
||||||
'fee_open': ANY,
|
|
||||||
'fee_open_cost': ANY,
|
|
||||||
'fee_open_currency': ANY,
|
|
||||||
'fee_close': fee.return_value,
|
|
||||||
'fee_close_cost': ANY,
|
|
||||||
'fee_close_currency': ANY,
|
|
||||||
'open_rate_requested': ANY,
|
|
||||||
'open_trade_value': 0.0010025,
|
|
||||||
'close_rate_requested': ANY,
|
|
||||||
'sell_reason': ANY,
|
|
||||||
'exit_reason': ANY,
|
|
||||||
'exit_order_status': ANY,
|
|
||||||
'min_rate': ANY,
|
|
||||||
'max_rate': ANY,
|
|
||||||
'strategy': ANY,
|
|
||||||
'buy_tag': ANY,
|
|
||||||
'enter_tag': ANY,
|
|
||||||
'timeframe': 5,
|
|
||||||
'open_order_id': ANY,
|
|
||||||
'close_date': None,
|
|
||||||
'close_timestamp': None,
|
|
||||||
'open_rate': 1.098e-05,
|
|
||||||
'close_rate': None,
|
|
||||||
'current_rate': 1.099e-05,
|
|
||||||
'amount': 91.07468124,
|
|
||||||
'amount_requested': 91.07468124,
|
|
||||||
'stake_amount': 0.001,
|
|
||||||
'trade_duration': None,
|
|
||||||
'trade_duration_s': None,
|
|
||||||
'close_profit': None,
|
|
||||||
'close_profit_pct': None,
|
|
||||||
'close_profit_abs': None,
|
|
||||||
'current_profit': 0.0,
|
|
||||||
'current_profit_pct': 0.0,
|
|
||||||
'current_profit_abs': 0.0,
|
|
||||||
'profit_ratio': 0.0,
|
|
||||||
'profit_pct': 0.0,
|
|
||||||
'profit_abs': 0.0,
|
|
||||||
'profit_fiat': ANY,
|
|
||||||
'stop_loss_abs': 0.0,
|
|
||||||
'stop_loss_pct': None,
|
|
||||||
'stop_loss_ratio': None,
|
|
||||||
'stoploss_order_id': None,
|
|
||||||
'stoploss_last_update': ANY,
|
|
||||||
'stoploss_last_update_timestamp': ANY,
|
|
||||||
'initial_stop_loss_abs': 0.0,
|
|
||||||
'initial_stop_loss_pct': None,
|
|
||||||
'initial_stop_loss_ratio': None,
|
|
||||||
'stoploss_current_dist': -1.099e-05,
|
|
||||||
'stoploss_current_dist_ratio': -1.0,
|
|
||||||
'stoploss_current_dist_pct': pytest.approx(-100.0),
|
|
||||||
'stoploss_entry_dist': -0.0010025,
|
|
||||||
'stoploss_entry_dist_ratio': -1.0,
|
|
||||||
'open_order': '(limit buy rem=91.07468123)',
|
|
||||||
'realized_profit': 0.0,
|
|
||||||
'exchange': 'binance',
|
|
||||||
'leverage': 1.0,
|
|
||||||
'interest_rate': 0.0,
|
|
||||||
'liquidation_price': None,
|
|
||||||
'is_short': False,
|
|
||||||
'funding_fees': 0.0,
|
|
||||||
'trading_mode': TradingMode.SPOT,
|
|
||||||
'orders': [{
|
|
||||||
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
|
|
||||||
'cost': 0.0009999999999054, 'filled': 0.0, 'ft_order_side': 'buy',
|
|
||||||
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
|
|
||||||
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
|
||||||
'is_open': True, 'pair': 'ETH/BTC', 'order_id': ANY,
|
|
||||||
'remaining': 91.07468123, 'status': ANY, 'ft_is_entry': True,
|
|
||||||
}],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Fill open order ...
|
|
||||||
freqtradebot.manage_open_orders()
|
|
||||||
trades = Trade.get_open_trades()
|
|
||||||
freqtradebot.exit_positions(trades)
|
|
||||||
|
|
||||||
results = rpc._rpc_trade_status()
|
|
||||||
assert results[0] == {
|
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'base_currency': 'ETH',
|
'base_currency': 'ETH',
|
||||||
@ -213,91 +108,103 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'remaining': ANY, 'status': ANY, 'ft_is_entry': True,
|
'remaining': ANY, 'status': ANY, 'ft_is_entry': True,
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
fetch_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
_is_dry_limit_order_filled=MagicMock(side_effect=[False, True]),
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
patch_get_signal(freqtradebot)
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
|
freqtradebot.state = State.RUNNING
|
||||||
|
with pytest.raises(RPCException, match=r'.*no active trade*'):
|
||||||
|
rpc._rpc_trade_status()
|
||||||
|
|
||||||
|
freqtradebot.enter_positions()
|
||||||
|
|
||||||
|
# Open order...
|
||||||
|
results = rpc._rpc_trade_status()
|
||||||
|
response_unfilled = deepcopy(gen_response)
|
||||||
|
# Different from "filled" response:
|
||||||
|
response_unfilled.update({
|
||||||
|
'amount': 91.07468124,
|
||||||
|
'profit_ratio': 0.0,
|
||||||
|
'profit_pct': 0.0,
|
||||||
|
'profit_abs': 0.0,
|
||||||
|
'current_profit': 0.0,
|
||||||
|
'current_profit_pct': 0.0,
|
||||||
|
'current_profit_abs': 0.0,
|
||||||
|
'stop_loss_abs': 0.0,
|
||||||
|
'stop_loss_pct': None,
|
||||||
|
'stop_loss_ratio': None,
|
||||||
|
'stoploss_current_dist': -1.099e-05,
|
||||||
|
'stoploss_current_dist_ratio': -1.0,
|
||||||
|
'stoploss_current_dist_pct': pytest.approx(-100.0),
|
||||||
|
'stoploss_entry_dist': -0.0010025,
|
||||||
|
'stoploss_entry_dist_ratio': -1.0,
|
||||||
|
'initial_stop_loss_abs': 0.0,
|
||||||
|
'initial_stop_loss_pct': None,
|
||||||
|
'initial_stop_loss_ratio': None,
|
||||||
|
'open_order': '(limit buy rem=91.07468123)',
|
||||||
|
})
|
||||||
|
response_unfilled['orders'][0].update({
|
||||||
|
'is_open': True,
|
||||||
|
'filled': 0.0,
|
||||||
|
'remaining': 91.07468123
|
||||||
|
})
|
||||||
|
assert results[0] == response_unfilled
|
||||||
|
|
||||||
|
# Open order without remaining
|
||||||
|
trade = Trade.get_open_trades()[0]
|
||||||
|
# kucoin case (no remaining set).
|
||||||
|
trade.orders[0].remaining = None
|
||||||
|
Trade.commit()
|
||||||
|
|
||||||
|
results = rpc._rpc_trade_status()
|
||||||
|
# Reuse above object, only remaining changed.
|
||||||
|
response_unfilled['orders'][0].update({
|
||||||
|
'remaining': None
|
||||||
|
})
|
||||||
|
assert results[0] == response_unfilled
|
||||||
|
|
||||||
|
trade = Trade.get_open_trades()[0]
|
||||||
|
trade.orders[0].remaining = trade.amount
|
||||||
|
Trade.commit()
|
||||||
|
|
||||||
|
# Fill open order ...
|
||||||
|
freqtradebot.manage_open_orders()
|
||||||
|
trades = Trade.get_open_trades()
|
||||||
|
freqtradebot.exit_positions(trades)
|
||||||
|
|
||||||
|
results = rpc._rpc_trade_status()
|
||||||
|
|
||||||
|
response = deepcopy(gen_response)
|
||||||
|
assert results[0] == response
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
||||||
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
|
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
|
||||||
results = rpc._rpc_trade_status()
|
results = rpc._rpc_trade_status()
|
||||||
assert isnan(results[0]['current_profit'])
|
assert isnan(results[0]['current_profit'])
|
||||||
assert isnan(results[0]['current_rate'])
|
assert isnan(results[0]['current_rate'])
|
||||||
assert results[0] == {
|
response_norate = deepcopy(gen_response)
|
||||||
'trade_id': 1,
|
# Update elements that are NaN when no rate is available.
|
||||||
'pair': 'ETH/BTC',
|
response_norate.update({
|
||||||
'base_currency': 'ETH',
|
|
||||||
'quote_currency': 'BTC',
|
|
||||||
'open_date': ANY,
|
|
||||||
'open_timestamp': ANY,
|
|
||||||
'is_open': ANY,
|
|
||||||
'fee_open': ANY,
|
|
||||||
'fee_open_cost': ANY,
|
|
||||||
'fee_open_currency': ANY,
|
|
||||||
'fee_close': fee.return_value,
|
|
||||||
'fee_close_cost': ANY,
|
|
||||||
'fee_close_currency': ANY,
|
|
||||||
'open_rate_requested': ANY,
|
|
||||||
'open_trade_value': ANY,
|
|
||||||
'close_rate_requested': ANY,
|
|
||||||
'sell_reason': ANY,
|
|
||||||
'exit_reason': ANY,
|
|
||||||
'exit_order_status': ANY,
|
|
||||||
'min_rate': ANY,
|
|
||||||
'max_rate': ANY,
|
|
||||||
'strategy': ANY,
|
|
||||||
'buy_tag': ANY,
|
|
||||||
'enter_tag': ANY,
|
|
||||||
'timeframe': ANY,
|
|
||||||
'open_order_id': ANY,
|
|
||||||
'close_date': None,
|
|
||||||
'close_timestamp': None,
|
|
||||||
'open_rate': 1.098e-05,
|
|
||||||
'close_rate': None,
|
|
||||||
'current_rate': ANY,
|
|
||||||
'amount': 91.07468123,
|
|
||||||
'amount_requested': 91.07468124,
|
|
||||||
'trade_duration': ANY,
|
|
||||||
'trade_duration_s': ANY,
|
|
||||||
'stake_amount': 0.001,
|
|
||||||
'close_profit': None,
|
|
||||||
'close_profit_pct': None,
|
|
||||||
'close_profit_abs': None,
|
|
||||||
'current_profit': ANY,
|
|
||||||
'current_profit_pct': ANY,
|
|
||||||
'current_profit_abs': ANY,
|
|
||||||
'profit_ratio': ANY,
|
|
||||||
'profit_pct': ANY,
|
|
||||||
'profit_abs': ANY,
|
|
||||||
'profit_fiat': ANY,
|
|
||||||
'stop_loss_abs': 9.89e-06,
|
|
||||||
'stop_loss_pct': -10.0,
|
|
||||||
'stop_loss_ratio': -0.1,
|
|
||||||
'stoploss_order_id': None,
|
|
||||||
'stoploss_last_update': ANY,
|
|
||||||
'stoploss_last_update_timestamp': ANY,
|
|
||||||
'initial_stop_loss_abs': 9.89e-06,
|
|
||||||
'initial_stop_loss_pct': -10.0,
|
|
||||||
'initial_stop_loss_ratio': -0.1,
|
|
||||||
'stoploss_current_dist': ANY,
|
'stoploss_current_dist': ANY,
|
||||||
'stoploss_current_dist_ratio': ANY,
|
'stoploss_current_dist_ratio': ANY,
|
||||||
'stoploss_current_dist_pct': ANY,
|
'stoploss_current_dist_pct': ANY,
|
||||||
'stoploss_entry_dist': -0.00010402,
|
'profit_ratio': ANY,
|
||||||
'stoploss_entry_dist_ratio': -0.10376381,
|
'profit_pct': ANY,
|
||||||
'open_order': None,
|
'profit_abs': ANY,
|
||||||
'exchange': 'binance',
|
'current_profit_abs': ANY,
|
||||||
'realized_profit': 0.0,
|
'current_profit': ANY,
|
||||||
'leverage': 1.0,
|
'current_profit_pct': ANY,
|
||||||
'interest_rate': 0.0,
|
'current_rate': ANY,
|
||||||
'liquidation_price': None,
|
})
|
||||||
'is_short': False,
|
assert results[0] == response_norate
|
||||||
'funding_fees': 0.0,
|
|
||||||
'trading_mode': TradingMode.SPOT,
|
|
||||||
'orders': [{
|
|
||||||
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
|
|
||||||
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
|
|
||||||
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
|
|
||||||
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
|
||||||
'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
|
|
||||||
'remaining': ANY, 'status': ANY, 'ft_is_entry': True,
|
|
||||||
}],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||||
@ -1149,6 +1056,10 @@ def test_rpc_force_entry(mocker, default_conf, ticker, fee, limit_buy_order_open
|
|||||||
assert trade.pair == pair
|
assert trade.pair == pair
|
||||||
assert trade.open_rate == 0.0001
|
assert trade.open_rate == 0.0001
|
||||||
|
|
||||||
|
with pytest.raises(RPCException,
|
||||||
|
match=r'Symbol does not exist or market is not active.'):
|
||||||
|
rpc._rpc_force_entry('LTC/NOTHING', 0.0001)
|
||||||
|
|
||||||
# Test buy pair not with stakes
|
# Test buy pair not with stakes
|
||||||
with pytest.raises(RPCException,
|
with pytest.raises(RPCException,
|
||||||
match=r'Wrong pair selected. Only pairs with stake-currency.*'):
|
match=r'Wrong pair selected. Only pairs with stake-currency.*'):
|
||||||
@ -1159,6 +1070,11 @@ def test_rpc_force_entry(mocker, default_conf, ticker, fee, limit_buy_order_open
|
|||||||
trade = rpc._rpc_force_entry(pair, 0.0001, order_type='limit', stake_amount=0.05)
|
trade = rpc._rpc_force_entry(pair, 0.0001, order_type='limit', stake_amount=0.05)
|
||||||
assert trade.stake_amount == 0.05
|
assert trade.stake_amount == 0.05
|
||||||
assert trade.buy_tag == 'force_entry'
|
assert trade.buy_tag == 'force_entry'
|
||||||
|
assert trade.open_order_id == 'mocked_limit_buy'
|
||||||
|
|
||||||
|
freqtradebot.strategy.position_adjustment_enable = True
|
||||||
|
with pytest.raises(RPCException, match=r'position for LTC/BTC already open.*open order.*'):
|
||||||
|
rpc._rpc_force_entry(pair, 0.0001, order_type='limit', stake_amount=0.05)
|
||||||
|
|
||||||
# Test not buying
|
# Test not buying
|
||||||
pair = 'XRP/BTC'
|
pair = 'XRP/BTC'
|
||||||
|
@ -57,7 +57,10 @@ def botclient(default_conf, mocker):
|
|||||||
try:
|
try:
|
||||||
apiserver = ApiServer(default_conf)
|
apiserver = ApiServer(default_conf)
|
||||||
apiserver.add_rpc_handler(rpc)
|
apiserver.add_rpc_handler(rpc)
|
||||||
yield ftbot, TestClient(apiserver.app)
|
# We need to use the TestClient as a context manager to
|
||||||
|
# handle lifespan events correctly
|
||||||
|
with TestClient(apiserver.app) as client:
|
||||||
|
yield ftbot, client
|
||||||
# Cleanup ... ?
|
# Cleanup ... ?
|
||||||
finally:
|
finally:
|
||||||
if apiserver:
|
if apiserver:
|
||||||
@ -438,7 +441,6 @@ def test_api_cleanup(default_conf, mocker, caplog):
|
|||||||
apiserver.cleanup()
|
apiserver.cleanup()
|
||||||
assert apiserver._server.cleanup.call_count == 1
|
assert apiserver._server.cleanup.call_count == 1
|
||||||
assert log_has("Stopping API Server", caplog)
|
assert log_has("Stopping API Server", caplog)
|
||||||
assert log_has("Stopping API Server background tasks", caplog)
|
|
||||||
ApiServer.shutdown()
|
ApiServer.shutdown()
|
||||||
|
|
||||||
|
|
||||||
@ -1459,6 +1461,7 @@ def test_api_strategies(botclient, tmpdir):
|
|||||||
'StrategyTestV3',
|
'StrategyTestV3',
|
||||||
'StrategyTestV3CustomEntryPrice',
|
'StrategyTestV3CustomEntryPrice',
|
||||||
'StrategyTestV3Futures',
|
'StrategyTestV3Futures',
|
||||||
|
'freqai_rl_test_strat',
|
||||||
'freqai_test_classifier',
|
'freqai_test_classifier',
|
||||||
'freqai_test_multimodel_classifier_strat',
|
'freqai_test_multimodel_classifier_strat',
|
||||||
'freqai_test_multimodel_strat',
|
'freqai_test_multimodel_strat',
|
||||||
@ -1714,12 +1717,14 @@ def test_api_ws_subscribe(botclient, mocker):
|
|||||||
|
|
||||||
with client.websocket_connect(ws_url) as ws:
|
with client.websocket_connect(ws_url) as ws:
|
||||||
ws.send_json({'type': 'subscribe', 'data': ['whitelist']})
|
ws.send_json({'type': 'subscribe', 'data': ['whitelist']})
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
# Check call count is now 1 as we sent a valid subscribe request
|
# Check call count is now 1 as we sent a valid subscribe request
|
||||||
assert sub_mock.call_count == 1
|
assert sub_mock.call_count == 1
|
||||||
|
|
||||||
with client.websocket_connect(ws_url) as ws:
|
with client.websocket_connect(ws_url) as ws:
|
||||||
ws.send_json({'type': 'subscribe', 'data': 'whitelist'})
|
ws.send_json({'type': 'subscribe', 'data': 'whitelist'})
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
# Call count hasn't changed as the subscribe request was invalid
|
# Call count hasn't changed as the subscribe request was invalid
|
||||||
assert sub_mock.call_count == 1
|
assert sub_mock.call_count == 1
|
||||||
@ -1773,24 +1778,18 @@ def test_api_ws_send_msg(default_conf, mocker, caplog):
|
|||||||
mocker.patch('freqtrade.rpc.api_server.ApiServer.start_api')
|
mocker.patch('freqtrade.rpc.api_server.ApiServer.start_api')
|
||||||
apiserver = ApiServer(default_conf)
|
apiserver = ApiServer(default_conf)
|
||||||
apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf)))
|
apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf)))
|
||||||
apiserver.start_message_queue()
|
|
||||||
# Give the queue thread time to start
|
|
||||||
time.sleep(0.2)
|
|
||||||
|
|
||||||
# Test message_queue coro receives the message
|
# Start test client context manager to run lifespan events
|
||||||
|
with TestClient(apiserver.app):
|
||||||
|
# Test message is published on the Message Stream
|
||||||
test_message = {"type": "status", "data": "test"}
|
test_message = {"type": "status", "data": "test"}
|
||||||
|
first_waiter = apiserver._message_stream._waiter
|
||||||
apiserver.send_msg(test_message)
|
apiserver.send_msg(test_message)
|
||||||
time.sleep(0.1) # Not sure how else to wait for the coro to receive the data
|
assert first_waiter.result()[0] == test_message
|
||||||
assert log_has("Found message of type: status", caplog)
|
|
||||||
|
|
||||||
# Test if exception logged when error occurs in sending
|
|
||||||
mocker.patch('freqtrade.rpc.api_server.ws.channel.ChannelManager.broadcast',
|
|
||||||
side_effect=Exception)
|
|
||||||
|
|
||||||
|
second_waiter = apiserver._message_stream._waiter
|
||||||
apiserver.send_msg(test_message)
|
apiserver.send_msg(test_message)
|
||||||
time.sleep(0.1) # Not sure how else to wait for the coro to receive the data
|
assert first_waiter != second_waiter
|
||||||
assert log_has_re(r"Exception happened in background task.*", caplog)
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
apiserver.cleanup()
|
|
||||||
ApiServer.shutdown()
|
ApiServer.shutdown()
|
||||||
|
105
tests/strategy/strats/freqai_rl_test_strat.py
Normal file
105
tests/strategy/strats/freqai_rl_test_strat.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import logging
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.strategy import IStrategy, merge_informative_pair
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class freqai_rl_test_strat(IStrategy):
|
||||||
|
"""
|
||||||
|
Test strategy - used for testing freqAI functionalities.
|
||||||
|
DO not use in production.
|
||||||
|
"""
|
||||||
|
|
||||||
|
minimal_roi = {"0": 0.1, "240": -1}
|
||||||
|
|
||||||
|
process_only_new_candles = True
|
||||||
|
stoploss = -0.05
|
||||||
|
use_exit_signal = True
|
||||||
|
startup_candle_count: int = 30
|
||||||
|
can_short = False
|
||||||
|
|
||||||
|
def populate_any_indicators(
|
||||||
|
self, pair, df, tf, informative=None, set_generalized_indicators=False
|
||||||
|
):
|
||||||
|
|
||||||
|
if informative is None:
|
||||||
|
informative = self.dp.get_pair_dataframe(pair, tf)
|
||||||
|
|
||||||
|
# first loop is automatically duplicating indicators for time periods
|
||||||
|
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
|
||||||
|
|
||||||
|
t = int(t)
|
||||||
|
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
|
||||||
|
|
||||||
|
# The following columns are necessary for RL models.
|
||||||
|
informative[f"%-{pair}raw_close"] = informative["close"]
|
||||||
|
informative[f"%-{pair}raw_open"] = informative["open"]
|
||||||
|
informative[f"%-{pair}raw_high"] = informative["high"]
|
||||||
|
informative[f"%-{pair}raw_low"] = informative["low"]
|
||||||
|
|
||||||
|
indicators = [col for col in informative if col.startswith("%")]
|
||||||
|
# This loop duplicates and shifts all indicators to add a sense of recency to data
|
||||||
|
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
|
||||||
|
if n == 0:
|
||||||
|
continue
|
||||||
|
informative_shift = informative[indicators].shift(n)
|
||||||
|
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
|
||||||
|
informative = pd.concat((informative, informative_shift), axis=1)
|
||||||
|
|
||||||
|
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
|
||||||
|
skip_columns = [
|
||||||
|
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
|
||||||
|
]
|
||||||
|
df = df.drop(columns=skip_columns)
|
||||||
|
|
||||||
|
# Add generalized indicators here (because in live, it will call this
|
||||||
|
# function to populate indicators during training). Notice how we ensure not to
|
||||||
|
# add them multiple times
|
||||||
|
if set_generalized_indicators:
|
||||||
|
# For RL, there are no direct targets to set. This is filler (neutral)
|
||||||
|
# until the agent sends an action.
|
||||||
|
df["&-action"] = 0
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
|
||||||
|
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
|
||||||
|
enter_long_conditions = [df["do_predict"] == 1, df["&-action"] == 1]
|
||||||
|
|
||||||
|
if enter_long_conditions:
|
||||||
|
df.loc[
|
||||||
|
reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"]
|
||||||
|
] = (1, "long")
|
||||||
|
|
||||||
|
enter_short_conditions = [df["do_predict"] == 1, df["&-action"] == 3]
|
||||||
|
|
||||||
|
if enter_short_conditions:
|
||||||
|
df.loc[
|
||||||
|
reduce(lambda x, y: x & y, enter_short_conditions), ["enter_short", "enter_tag"]
|
||||||
|
] = (1, "short")
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
exit_long_conditions = [df["do_predict"] == 1, df["&-action"] == 2]
|
||||||
|
if exit_long_conditions:
|
||||||
|
df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1
|
||||||
|
|
||||||
|
exit_short_conditions = [df["do_predict"] == 1, df["&-action"] == 4]
|
||||||
|
if exit_short_conditions:
|
||||||
|
df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1
|
||||||
|
|
||||||
|
return df
|
@ -34,7 +34,7 @@ def test_search_all_strategies_no_failed():
|
|||||||
directory = Path(__file__).parent / "strats"
|
directory = Path(__file__).parent / "strats"
|
||||||
strategies = StrategyResolver._search_all_objects(directory, enum_failed=False)
|
strategies = StrategyResolver._search_all_objects(directory, enum_failed=False)
|
||||||
assert isinstance(strategies, list)
|
assert isinstance(strategies, list)
|
||||||
assert len(strategies) == 11
|
assert len(strategies) == 12
|
||||||
assert isinstance(strategies[0], dict)
|
assert isinstance(strategies[0], dict)
|
||||||
|
|
||||||
|
|
||||||
@ -42,10 +42,10 @@ def test_search_all_strategies_with_failed():
|
|||||||
directory = Path(__file__).parent / "strats"
|
directory = Path(__file__).parent / "strats"
|
||||||
strategies = StrategyResolver._search_all_objects(directory, enum_failed=True)
|
strategies = StrategyResolver._search_all_objects(directory, enum_failed=True)
|
||||||
assert isinstance(strategies, list)
|
assert isinstance(strategies, list)
|
||||||
assert len(strategies) == 12
|
assert len(strategies) == 13
|
||||||
# with enum_failed=True search_all_objects() shall find 2 good strategies
|
# with enum_failed=True search_all_objects() shall find 2 good strategies
|
||||||
# and 1 which fails to load
|
# and 1 which fails to load
|
||||||
assert len([x for x in strategies if x['class'] is not None]) == 11
|
assert len([x for x in strategies if x['class'] is not None]) == 12
|
||||||
|
|
||||||
assert len([x for x in strategies if x['class'] is None]) == 1
|
assert len([x for x in strategies if x['class'] is None]) == 1
|
||||||
|
|
||||||
|
@ -1498,6 +1498,7 @@ def test_handle_stoploss_on_exchange_trailing(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
assert freqtrade.handle_trade(trade) is True
|
assert freqtrade.handle_trade(trade) is True
|
||||||
|
assert trade.stoploss_order_id is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
@ -5046,7 +5047,7 @@ def test_startup_backpopulate_precision(mocker, default_conf_usdt, fee, caplog):
|
|||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
def test_update_closed_trades_without_assigned_fees(mocker, default_conf_usdt, fee, is_short):
|
def test_update_trades_without_assigned_fees(mocker, default_conf_usdt, fee, is_short):
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
|
||||||
def patch_with_fee(order):
|
def patch_with_fee(order):
|
||||||
@ -5075,7 +5076,7 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf_usdt, f
|
|||||||
assert trade.fee_close_cost is None
|
assert trade.fee_close_cost is None
|
||||||
assert trade.fee_close_currency is None
|
assert trade.fee_close_currency is None
|
||||||
|
|
||||||
freqtrade.update_closed_trades_without_assigned_fees()
|
freqtrade.update_trades_without_assigned_fees()
|
||||||
|
|
||||||
# Does nothing for dry-run
|
# Does nothing for dry-run
|
||||||
trades = Trade.get_trades().all()
|
trades = Trade.get_trades().all()
|
||||||
@ -5088,7 +5089,7 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf_usdt, f
|
|||||||
|
|
||||||
freqtrade.config['dry_run'] = False
|
freqtrade.config['dry_run'] = False
|
||||||
|
|
||||||
freqtrade.update_closed_trades_without_assigned_fees()
|
freqtrade.update_trades_without_assigned_fees()
|
||||||
|
|
||||||
trades = Trade.get_trades().all()
|
trades = Trade.get_trades().all()
|
||||||
assert len(trades) == MOCK_TRADE_COUNT
|
assert len(trades) == MOCK_TRADE_COUNT
|
||||||
@ -5551,7 +5552,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
|
|||||||
assert trade.stake_amount == 110
|
assert trade.stake_amount == 110
|
||||||
|
|
||||||
# Assume it does nothing since order is closed and trade is open
|
# Assume it does nothing since order is closed and trade is open
|
||||||
freqtrade.update_closed_trades_without_assigned_fees()
|
freqtrade.update_trades_without_assigned_fees()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade
|
assert trade
|
||||||
@ -5622,7 +5623,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.create_order', fetch_order_mm)
|
mocker.patch('freqtrade.exchange.Exchange.create_order', fetch_order_mm)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order', fetch_order_mm)
|
mocker.patch('freqtrade.exchange.Exchange.fetch_order', fetch_order_mm)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', fetch_order_mm)
|
mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', fetch_order_mm)
|
||||||
freqtrade.update_closed_trades_without_assigned_fees()
|
freqtrade.update_trades_without_assigned_fees()
|
||||||
|
|
||||||
orders = Order.query.all()
|
orders = Order.query.all()
|
||||||
assert orders
|
assert orders
|
||||||
@ -5839,7 +5840,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None:
|
|||||||
assert trade.stake_amount == bid * amount
|
assert trade.stake_amount == bid * amount
|
||||||
|
|
||||||
# Assume it does nothing since order is closed and trade is open
|
# Assume it does nothing since order is closed and trade is open
|
||||||
freqtrade.update_closed_trades_without_assigned_fees()
|
freqtrade.update_trades_without_assigned_fees()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade
|
assert trade
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -8,16 +10,28 @@ from freqtrade.exceptions import OperationalException
|
|||||||
|
|
||||||
def test_parse_timerange_incorrect():
|
def test_parse_timerange_incorrect():
|
||||||
|
|
||||||
assert TimeRange('date', None, 1274486400, 0) == TimeRange.parse_timerange('20100522-')
|
timerange = TimeRange.parse_timerange('20100522-')
|
||||||
assert TimeRange(None, 'date', 0, 1274486400) == TimeRange.parse_timerange('-20100522')
|
assert TimeRange('date', None, 1274486400, 0) == timerange
|
||||||
|
assert timerange.timerange_str == '20100522-'
|
||||||
|
timerange = TimeRange.parse_timerange('-20100522')
|
||||||
|
assert TimeRange(None, 'date', 0, 1274486400) == timerange
|
||||||
|
assert timerange.timerange_str == '-20100522'
|
||||||
timerange = TimeRange.parse_timerange('20100522-20150730')
|
timerange = TimeRange.parse_timerange('20100522-20150730')
|
||||||
assert timerange == TimeRange('date', 'date', 1274486400, 1438214400)
|
assert timerange == TimeRange('date', 'date', 1274486400, 1438214400)
|
||||||
|
assert timerange.timerange_str == '20100522-20150730'
|
||||||
|
assert timerange.start_fmt == '2010-05-22 00:00:00'
|
||||||
|
assert timerange.stop_fmt == '2015-07-30 00:00:00'
|
||||||
|
|
||||||
# Added test for unix timestamp - BTC genesis date
|
# Added test for unix timestamp - BTC genesis date
|
||||||
assert TimeRange('date', None, 1231006505, 0) == TimeRange.parse_timerange('1231006505-')
|
assert TimeRange('date', None, 1231006505, 0) == TimeRange.parse_timerange('1231006505-')
|
||||||
assert TimeRange(None, 'date', 0, 1233360000) == TimeRange.parse_timerange('-1233360000')
|
assert TimeRange(None, 'date', 0, 1233360000) == TimeRange.parse_timerange('-1233360000')
|
||||||
timerange = TimeRange.parse_timerange('1231006505-1233360000')
|
timerange = TimeRange.parse_timerange('1231006505-1233360000')
|
||||||
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
|
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
|
||||||
|
assert isinstance(timerange.startdt, datetime)
|
||||||
|
assert isinstance(timerange.stopdt, datetime)
|
||||||
|
assert timerange.startdt == datetime.fromtimestamp(1231006505, tz=timezone.utc)
|
||||||
|
assert timerange.stopdt == datetime.fromtimestamp(1233360000, tz=timezone.utc)
|
||||||
|
assert timerange.timerange_str == '20090103-20090131'
|
||||||
|
|
||||||
timerange = TimeRange.parse_timerange('1231006505000-1233360000000')
|
timerange = TimeRange.parse_timerange('1231006505000-1233360000000')
|
||||||
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
|
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
|
||||||
@ -45,6 +59,7 @@ def test_subtract_start():
|
|||||||
x = TimeRange(None, 'date', 0, 1438214400)
|
x = TimeRange(None, 'date', 0, 1438214400)
|
||||||
x.subtract_start(300)
|
x.subtract_start(300)
|
||||||
assert not x.startts
|
assert not x.startts
|
||||||
|
assert not x.startdt
|
||||||
|
|
||||||
x = TimeRange('date', None, 1274486400, 0)
|
x = TimeRange('date', None, 1274486400, 0)
|
||||||
x.subtract_start(300)
|
x.subtract_start(300)
|
||||||
|
10
tests/testdata/strategy_SampleStrategy.fthypt
vendored
10
tests/testdata/strategy_SampleStrategy.fthypt
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user