Merge branch 'nullart-nullartHFT' into develop
This commit is contained in:
commit
e35c041081
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -1,6 +1,6 @@
|
|||||||
## Step 1: Have you search for this issue before posting it?
|
## Step 1: Have you search for this issue before posting it?
|
||||||
|
|
||||||
If you have discovered a bug in the bot, please [search our issue tracker](https://github.com/gcarq/freqtrade/issues?q=is%3Aissue).
|
If you have discovered a bug in the bot, please [search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue).
|
||||||
If it hasn't been reported, please create a new issue.
|
If it hasn't been reported, please create a new issue.
|
||||||
|
|
||||||
## Step 2: Describe your environment
|
## Step 2: Describe your environment
|
||||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,5 +1,5 @@
|
|||||||
Thank you for sending your pull request. But first, have you included
|
Thank you for sending your pull request. But first, have you included
|
||||||
unit tests, and is your code PEP8 conformant? [More details](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
|
unit tests, and is your code PEP8 conformant? [More details](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
Explain in one sentence the goal of this PR
|
Explain in one sentence the goal of this PR
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -8,6 +8,7 @@ logfile.txt
|
|||||||
hyperopt_trials.pickle
|
hyperopt_trials.pickle
|
||||||
user_data/
|
user_data/
|
||||||
freqtrade-plot.html
|
freqtrade-plot.html
|
||||||
|
freqtrade-profit-plot.html
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
@ -90,3 +91,4 @@ target/
|
|||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
4
.pyup.yml
Normal file
4
.pyup.yml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# autogenerated pyup.io config file
|
||||||
|
# see https://pyup.io/docs/configuration/ for all available options
|
||||||
|
|
||||||
|
schedule: every day
|
13
.travis.yml
13
.travis.yml
@ -13,21 +13,22 @@ addons:
|
|||||||
install:
|
install:
|
||||||
- ./install_ta-lib.sh
|
- ./install_ta-lib.sh
|
||||||
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
||||||
- pip install --upgrade flake8 coveralls pytest-random-order
|
- pip install --upgrade flake8 coveralls pytest-random-order mypy
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- pip install -e .
|
- pip install -e .
|
||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
- script: pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
|
- script:
|
||||||
|
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
|
||||||
|
- coveralls
|
||||||
- script:
|
- script:
|
||||||
- cp config.json.example config.json
|
- cp config.json.example config.json
|
||||||
- python freqtrade/main.py backtesting
|
- python freqtrade/main.py --datadir freqtrade/tests/testdata backtesting
|
||||||
- script:
|
- script:
|
||||||
- cp config.json.example config.json
|
- cp config.json.example config.json
|
||||||
- python freqtrade/main.py hyperopt -e 5
|
- python freqtrade/main.py --datadir freqtrade/tests/testdata hyperopt -e 5
|
||||||
- script: flake8 freqtrade
|
- script: flake8 freqtrade
|
||||||
after_success:
|
- script: mypy freqtrade
|
||||||
- coveralls
|
|
||||||
notifications:
|
notifications:
|
||||||
slack:
|
slack:
|
||||||
secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q=
|
secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q=
|
||||||
|
@ -7,7 +7,7 @@ Feel like our bot is missing a feature? We welcome your pull requests! Few point
|
|||||||
conformant (max-line-length = 100).
|
conformant (max-line-length = 100).
|
||||||
|
|
||||||
If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE)
|
If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE)
|
||||||
or in a [issue](https://github.com/gcarq/freqtrade/issues) before a PR.
|
or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
|
||||||
|
|
||||||
|
|
||||||
**Before sending the PR:**
|
**Before sending the PR:**
|
||||||
@ -42,4 +42,16 @@ pip3.6 install flake8 coveralls
|
|||||||
flake8 freqtrade
|
flake8 freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 3. Test if all type-hints are correct
|
||||||
|
|
||||||
|
**Install packages** (If not already installed)
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
pip3.6 install mypy
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run mypy**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
mypy freqtrade
|
||||||
|
```
|
||||||
|
93
README.md
93
README.md
@ -1,14 +1,14 @@
|
|||||||
# freqtrade
|
# freqtrade
|
||||||
|
|
||||||
[](https://travis-ci.org/gcarq/freqtrade)
|
[](https://travis-ci.org/freqtrade/freqtrade)
|
||||||
[](https://coveralls.io/github/gcarq/freqtrade?branch=develop)
|
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||||
[](https://codeclimate.com/github/gcarq/freqtrade/maintainability)
|
[](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
||||||
|
|
||||||
|
|
||||||
Simple High frequency trading bot for crypto currencies designed to
|
Simple High frequency trading bot for crypto currencies designed to
|
||||||
support multi exchanges and be controlled via Telegram.
|
support multi exchanges and be controlled via Telegram.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
This software is for educational purposes only. Do not risk money which
|
This software is for educational purposes only. Do not risk money which
|
||||||
@ -25,12 +25,12 @@ hesitate to read the source code and understand the mechanism of this bot.
|
|||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Quick start](#quick-start)
|
- [Quick start](#quick-start)
|
||||||
- [Documentations](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
|
- [Documentations](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
||||||
- [Installation](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md)
|
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
|
||||||
- [Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)
|
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
|
||||||
- [Strategy Optimization](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md)
|
- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
|
||||||
- [Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/backtesting.md)
|
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
||||||
- [Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/hyperopt.md)
|
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||||
- [Support](#support)
|
- [Support](#support)
|
||||||
- [Help](#help--slack)
|
- [Help](#help--slack)
|
||||||
- [Bugs](#bugs--issues)
|
- [Bugs](#bugs--issues)
|
||||||
@ -56,29 +56,24 @@ Windows, macOS and Linux
|
|||||||
- [x] **Persistence**: Persistence is achieved through sqlite
|
- [x] **Persistence**: Persistence is achieved through sqlite
|
||||||
- [x] **Dry-run**: Run the bot without playing money.
|
- [x] **Dry-run**: Run the bot without playing money.
|
||||||
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
||||||
- [x] **Strategy Optimization**: Optimize your buy/sell strategy
|
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell
|
||||||
parameters with Hyperopts.
|
strategy parameters with real exchange data.
|
||||||
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you
|
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade.
|
||||||
want to trade.
|
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
|
||||||
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you
|
|
||||||
want to avoid.
|
|
||||||
- [x] **Manageable via Telegram**: Manage the bot with Telegram
|
- [x] **Manageable via Telegram**: Manage the bot with Telegram
|
||||||
- [x] **Display profit/loss in fiat**: Display your profit/loss in
|
- [x] **Display profit/loss in fiat**: Display your profit/loss in 33 fiat.
|
||||||
33 fiat.
|
- [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss.
|
||||||
- [x] **Daily summary of profit/loss**: Provide a daily summary
|
- [x] **Performance status report**: Provide a performance status of your current trades.
|
||||||
of your profit/loss.
|
|
||||||
- [x] **Performance status report**: Provide a performance status of
|
|
||||||
your current trades.
|
|
||||||
|
|
||||||
### Exchange supported
|
### Exchange marketplaces supported
|
||||||
- [x] Bittrex
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [ ] Binance
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [ ] Others
|
- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
This quick start section is a very short explanation on how to test the
|
This quick start section is a very short explanation on how to test the
|
||||||
bot in dry-run. We invite you to read the
|
bot in dry-run. We invite you to read the
|
||||||
[bot documentation](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
|
[bot documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
||||||
to ensure you understand how the bot is working.
|
to ensure you understand how the bot is working.
|
||||||
|
|
||||||
### Easy installation
|
### Easy installation
|
||||||
@ -92,7 +87,7 @@ The following steps are made for Linux/MacOS environment
|
|||||||
|
|
||||||
**1. Clone the repo**
|
**1. Clone the repo**
|
||||||
```bash
|
```bash
|
||||||
git clone git@github.com:gcarq/freqtrade.git
|
git clone git@github.com:freqtrade/freqtrade.git
|
||||||
git checkout develop
|
git checkout develop
|
||||||
cd freqtrade
|
cd freqtrade
|
||||||
```
|
```
|
||||||
@ -114,26 +109,26 @@ For any questions not covered by the documentation or for further
|
|||||||
information about the bot, we encourage you to join our slack channel.
|
information about the bot, we encourage you to join our slack channel.
|
||||||
- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE).
|
- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE).
|
||||||
|
|
||||||
### [Bugs / Issues](https://github.com/gcarq/freqtrade/issues?q=is%3Aissue)
|
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
||||||
If you discover a bug in the bot, please
|
If you discover a bug in the bot, please
|
||||||
[search our issue tracker](https://github.com/gcarq/freqtrade/issues?q=is%3Aissue)
|
[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
||||||
first. If it hasn't been reported, please
|
first. If it hasn't been reported, please
|
||||||
[create a new issue](https://github.com/gcarq/freqtrade/issues/new) and
|
[create a new issue](https://github.com/freqtrade/freqtrade/issues/new) and
|
||||||
ensure you follow the template guide so that our team can assist you as
|
ensure you follow the template guide so that our team can assist you as
|
||||||
quickly as possible.
|
quickly as possible.
|
||||||
|
|
||||||
### [Feature Requests](https://github.com/gcarq/freqtrade/labels/enhancement)
|
### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement)
|
||||||
Have you a great idea to improve the bot you want to share? Please,
|
Have you a great idea to improve the bot you want to share? Please,
|
||||||
first search if this feature was not [already discussed](https://github.com/gcarq/freqtrade/labels/enhancement).
|
first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement).
|
||||||
If it hasn't been requested, please
|
If it hasn't been requested, please
|
||||||
[create a new request](https://github.com/gcarq/freqtrade/issues/new)
|
[create a new request](https://github.com/freqtrade/freqtrade/issues/new)
|
||||||
and ensure you follow the template guide so that it does not get lost
|
and ensure you follow the template guide so that it does not get lost
|
||||||
in the bug reports.
|
in the bug reports.
|
||||||
|
|
||||||
### [Pull Requests](https://github.com/gcarq/freqtrade/pulls)
|
### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls)
|
||||||
Feel like our bot is missing a feature? We welcome your pull requests!
|
Feel like our bot is missing a feature? We welcome your pull requests!
|
||||||
Please read our
|
Please read our
|
||||||
[Contributing document](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
|
[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
to understand the requirements before sending your pull-requests.
|
to understand the requirements before sending your pull-requests.
|
||||||
|
|
||||||
**Important:** Always create your PR against the `develop` branch, not
|
**Important:** Always create your PR against the `develop` branch, not
|
||||||
@ -144,8 +139,9 @@ to understand the requirements before sending your pull-requests.
|
|||||||
### Bot commands
|
### Bot commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
usage: main.py [-h] [-v] [--version] [-c PATH] [--dry-run-db] [--datadir PATH]
|
usage: main.py [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
|
||||||
[--dynamic-whitelist [INT]]
|
[--strategy-path PATH] [--dynamic-whitelist [INT]]
|
||||||
|
[--dry-run-db]
|
||||||
{backtesting,hyperopt} ...
|
{backtesting,hyperopt} ...
|
||||||
|
|
||||||
Simple High Frequency Trading Bot for crypto currencies
|
Simple High Frequency Trading Bot for crypto currencies
|
||||||
@ -161,23 +157,28 @@ optional arguments:
|
|||||||
--version show program's version number and exit
|
--version show program's version number and exit
|
||||||
-c PATH, --config PATH
|
-c PATH, --config PATH
|
||||||
specify configuration file (default: config.json)
|
specify configuration file (default: config.json)
|
||||||
--dry-run-db Force dry run to use a local DB
|
-d PATH, --datadir PATH
|
||||||
"tradesv3.dry_run.sqlite" instead of memory DB. Work
|
path to backtest data (default:
|
||||||
only if dry_run is enabled.
|
freqtrade/tests/testdata
|
||||||
--datadir PATH path to backtest data (default freqdata/tests/testdata
|
-s NAME, --strategy NAME
|
||||||
|
specify strategy class name (default: DefaultStrategy)
|
||||||
|
--strategy-path PATH specify additional strategy lookup path
|
||||||
--dynamic-whitelist [INT]
|
--dynamic-whitelist [INT]
|
||||||
dynamically generate and update whitelist based on 24h
|
dynamically generate and update whitelist based on 24h
|
||||||
BaseVolume (Default 20 currencies)
|
BaseVolume (Default 20 currencies)
|
||||||
|
--dry-run-db Force dry run to use a local DB
|
||||||
|
"tradesv3.dry_run.sqlite" instead of memory DB. Work
|
||||||
|
only if dry_run is enabled.
|
||||||
```
|
```
|
||||||
More details on:
|
More details on:
|
||||||
- [How to run the bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
|
- [How to run the bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
|
||||||
- [How to use Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
|
- [How to use Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
|
||||||
- [How to use Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
|
- [How to use Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
|
||||||
|
|
||||||
### Telegram RPC commands
|
### Telegram RPC commands
|
||||||
Telegram is not mandatory. However, this is a great way to control your
|
Telegram is not mandatory. However, this is a great way to control your
|
||||||
bot. More details on our
|
bot. More details on our
|
||||||
[documentation](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
|
[documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
||||||
|
|
||||||
- `/start`: Starts the trader
|
- `/start`: Starts the trader
|
||||||
- `/stop`: Stops the trader
|
- `/stop`: Stops the trader
|
||||||
|
@ -7,7 +7,14 @@
|
|||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
"unfilledtimeout": 600,
|
"unfilledtimeout": 600,
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0
|
"ask_last_balance": 0.0,
|
||||||
|
"use_book_order": true,
|
||||||
|
"book_order_top": 6
|
||||||
|
},
|
||||||
|
"ask_strategy":{
|
||||||
|
"use_book_order": true,
|
||||||
|
"book_order_min": 1,
|
||||||
|
"book_order_max": 30
|
||||||
},
|
},
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
|
@ -14,7 +14,14 @@
|
|||||||
"stoploss": -0.10,
|
"stoploss": -0.10,
|
||||||
"unfilledtimeout": 600,
|
"unfilledtimeout": 600,
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0
|
"ask_last_balance": 0.0,
|
||||||
|
"use_book_order": true,
|
||||||
|
"book_order_top": 6
|
||||||
|
},
|
||||||
|
"ask_strategy":{
|
||||||
|
"use_book_order": true,
|
||||||
|
"book_order_min": 1,
|
||||||
|
"book_order_max": 30
|
||||||
},
|
},
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
@ -45,6 +52,7 @@
|
|||||||
"token": "your_telegram_token",
|
"token": "your_telegram_token",
|
||||||
"chat_id": "your_telegram_chat_id"
|
"chat_id": "your_telegram_chat_id"
|
||||||
},
|
},
|
||||||
|
"db_url": "sqlite:///tradesv3.sqlite",
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"internals": {
|
"internals": {
|
||||||
"process_throttle_secs": 5
|
"process_throttle_secs": 5
|
||||||
|
@ -14,7 +14,7 @@ real data. This is what we call
|
|||||||
|
|
||||||
Backtesting will use the crypto-currencies (pair) from your config file
|
Backtesting will use the crypto-currencies (pair) from your config file
|
||||||
and load static tickers located in
|
and load static tickers located in
|
||||||
[/freqtrade/tests/testdata](https://github.com/gcarq/freqtrade/tree/develop/freqtrade/tests/testdata).
|
[/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata).
|
||||||
If the 5 min and 1 min ticker for the crypto-currencies to test is not
|
If the 5 min and 1 min ticker for the crypto-currencies to test is not
|
||||||
already in the `testdata` folder, backtesting will download them
|
already in the `testdata` folder, backtesting will download them
|
||||||
automatically. Testdata files will not be updated until you specify it.
|
automatically. Testdata files will not be updated until you specify it.
|
||||||
@ -53,15 +53,21 @@ python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180
|
|||||||
|
|
||||||
**With a (custom) strategy file**
|
**With a (custom) strategy file**
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py -s currentstrategy backtesting
|
python3 ./freqtrade/main.py -s TestStrategy backtesting
|
||||||
```
|
```
|
||||||
Where `-s currentstrategy` refers to a filename `currentstrategy.py` in `freqtrade/user_data/strategies`
|
Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory
|
||||||
|
|
||||||
**Exporting trades to file**
|
**Exporting trades to file**
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --export trades
|
python3 ./freqtrade/main.py backtesting --export trades
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Exporting trades to file specifying a custom filename**
|
||||||
|
```bash
|
||||||
|
python3 ./freqtrade/main.py backtesting --export trades --export-filename=backtest_teststrategy.json
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
**Running backtest with smaller testset**
|
**Running backtest with smaller testset**
|
||||||
Use the `--timerange` argument to change how much of the testset
|
Use the `--timerange` argument to change how much of the testset
|
||||||
you want to use. The last N ticks/timeframes will be used.
|
you want to use. The last N ticks/timeframes will be used.
|
||||||
@ -83,24 +89,35 @@ The full timerange specification:
|
|||||||
- Use tickframes till 2018/01/31: `--timerange=-20180131`
|
- Use tickframes till 2018/01/31: `--timerange=-20180131`
|
||||||
- Use tickframes since 2018/01/31: `--timerange=20180131-`
|
- Use tickframes since 2018/01/31: `--timerange=20180131-`
|
||||||
- Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
|
- Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
|
||||||
|
- Use tickframes between POSIX timestamps 1527595200 1527618600:
|
||||||
|
`--timerange=1527595200-1527618600`
|
||||||
|
|
||||||
|
|
||||||
**Update testdata directory**
|
**Downloading new set of ticker data**
|
||||||
To update your testdata directory, or download into another testdata directory:
|
To download new set of backtesting ticker data, you can use a download script.
|
||||||
```bash
|
|
||||||
mkdir -p user_data/data/testdata-20180113
|
|
||||||
cp freqtrade/tests/testdata/pairs.json user_data/data-20180113
|
|
||||||
cd user_data/data-20180113
|
|
||||||
```
|
|
||||||
|
|
||||||
Possibly edit pairs.json file to include/exclude pairs
|
If you are using Binance for example:
|
||||||
|
- create a folder `user_data/data/binance` and copy `pairs.json` in that folder.
|
||||||
|
- update the `pairs.json` to contain the currency pairs you are interested in.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 freqtrade/tests/testdata/download_backtest_data.py -p pairs.json
|
mkdir -p user_data/data/binance
|
||||||
|
cp freqtrade/tests/testdata/pairs.json user_data/data/binance
|
||||||
```
|
```
|
||||||
|
|
||||||
The script will read your pairs.json file, and download ticker data
|
Then run:
|
||||||
into the current working directory.
|
|
||||||
|
```bash
|
||||||
|
python scripts/download_backtest_data --exchange binance
|
||||||
|
```
|
||||||
|
|
||||||
|
This will download ticker data for all the currency pairs you defined in `pairs.json`.
|
||||||
|
|
||||||
|
- To use a different folder than the exchange specific default, use `--export user_data/data/some_directory`.
|
||||||
|
- To change the exchange used to download the tickers, use `--exchange`. Default is `bittrex`.
|
||||||
|
- To use `pairs.json` from some other folder, use `--pairs-file some_other_dir/pairs.json`.
|
||||||
|
- To download ticker data for only 10 days, use `--days 10`.
|
||||||
|
- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers.
|
||||||
|
|
||||||
|
|
||||||
For help about backtesting usage, please refer to
|
For help about backtesting usage, please refer to
|
||||||
@ -158,4 +175,4 @@ strategies, your configuration, and the crypto-currency you have set up.
|
|||||||
## Next step
|
## Next step
|
||||||
Great, your strategy is profitable. What if the bot can give your the
|
Great, your strategy is profitable. What if the bot can give your the
|
||||||
optimal parameters to use for your strategy?
|
optimal parameters to use for your strategy?
|
||||||
Your next step is to learn [how to find optimal parameters with Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/hyperopt.md)
|
Your next step is to learn [how to find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
# Bot Optimization
|
# Bot Optimization
|
||||||
|
|
||||||
This page explains where to customize your strategies, and add new
|
This page explains where to customize your strategies, and add new
|
||||||
indicators.
|
indicators.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Install a custom strategy file](#install-a-custom-strategy-file)
|
- [Install a custom strategy file](#install-a-custom-strategy-file)
|
||||||
- [Customize your strategy](#change-your-strategy)
|
- [Customize your strategy](#change-your-strategy)
|
||||||
- [Add more Indicator](#add-more-indicator)
|
- [Add more Indicator](#add-more-indicator)
|
||||||
@ -11,10 +13,12 @@ indicators.
|
|||||||
Since the version `0.16.0` the bot allows using custom strategy file.
|
Since the version `0.16.0` the bot allows using custom strategy file.
|
||||||
|
|
||||||
## Install a custom strategy file
|
## Install a custom strategy file
|
||||||
|
|
||||||
This is very simple. Copy paste your strategy file into the folder
|
This is very simple. Copy paste your strategy file into the folder
|
||||||
`user_data/strategies`.
|
`user_data/strategies`.
|
||||||
|
|
||||||
Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`:
|
Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`:
|
||||||
|
|
||||||
1. Move your file into `user_data/strategies` (you should have `user_data/strategies/awesome-strategy.py`
|
1. Move your file into `user_data/strategies` (you should have `user_data/strategies/awesome-strategy.py`
|
||||||
2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name)
|
2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name)
|
||||||
|
|
||||||
@ -23,12 +27,14 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Change your strategy
|
## Change your strategy
|
||||||
|
|
||||||
The bot includes a default strategy file. However, we recommend you to
|
The bot includes a default strategy file. However, we recommend you to
|
||||||
use your own file to not have to lose your parameters every time the default
|
use your own file to not have to lose your parameters every time the default
|
||||||
strategy file will be updated on Github. Put your custom strategy file
|
strategy file will be updated on Github. Put your custom strategy file
|
||||||
into the folder `user_data/strategies`.
|
into the folder `user_data/strategies`.
|
||||||
|
|
||||||
A strategy file contains all the information needed to build a good strategy:
|
A strategy file contains all the information needed to build a good strategy:
|
||||||
|
|
||||||
- Buy strategy rules
|
- Buy strategy rules
|
||||||
- Sell strategy rules
|
- Sell strategy rules
|
||||||
- Minimal ROI recommended
|
- Minimal ROI recommended
|
||||||
@ -38,25 +44,28 @@ A strategy file contains all the information needed to build a good strategy:
|
|||||||
The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`.
|
The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`.
|
||||||
You can test it with the parameter: `--strategy TestStrategy`
|
You can test it with the parameter: `--strategy TestStrategy`
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
python3 ./freqtrade/main.py --strategy AwesomeStrategy
|
python3 ./freqtrade/main.py --strategy AwesomeStrategy
|
||||||
```
|
```
|
||||||
|
|
||||||
### Specify custom strategy location
|
### Specify custom strategy location
|
||||||
|
|
||||||
If you want to use a strategy from a different folder you can pass `--strategy-path`
|
If you want to use a strategy from a different folder you can pass `--strategy-path`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/folder
|
python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/folder
|
||||||
```
|
```
|
||||||
|
|
||||||
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
||||||
file as reference.**
|
file as reference.**
|
||||||
|
|
||||||
### Buy strategy
|
### Buy strategy
|
||||||
|
|
||||||
Edit the method `populate_buy_trend()` into your strategy file to
|
Edit the method `populate_buy_trend()` into your strategy file to
|
||||||
update your buy strategy.
|
update your buy strategy.
|
||||||
|
|
||||||
Sample from `user_data/strategies/test_strategy.py`:
|
Sample from `user_data/strategies/test_strategy.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@ -76,10 +85,11 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Sell strategy
|
### Sell strategy
|
||||||
Edit the method `populate_sell_trend()` into your strategy file to
|
|
||||||
update your sell strategy.
|
Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy.
|
||||||
|
|
||||||
Sample from `user_data/strategies/test_strategy.py`:
|
Sample from `user_data/strategies/test_strategy.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@ -98,11 +108,13 @@ def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Add more Indicator
|
## Add more Indicator
|
||||||
|
|
||||||
As you have seen, buy and sell strategies need indicators. You can add
|
As you have seen, buy and sell strategies need indicators. You can add
|
||||||
more indicators by extending the list contained in
|
more indicators by extending the list contained in
|
||||||
the method `populate_indicators()` from your strategy file.
|
the method `populate_indicators()` from your strategy file.
|
||||||
|
|
||||||
Sample:
|
Sample:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@ -137,16 +149,25 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
|||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
**Want more indicators example?**
|
### Want more indicator examples
|
||||||
Look into the [user_data/strategies/test_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py).
|
|
||||||
|
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).
|
||||||
Then uncomment indicators you need.
|
Then uncomment indicators you need.
|
||||||
|
|
||||||
|
|
||||||
### Where is the default strategy?
|
### Where is the default strategy?
|
||||||
The default buy strategy is located in the file
|
|
||||||
[freqtrade/default_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/strategy/default_strategy.py).
|
|
||||||
|
|
||||||
|
The default buy strategy is located in the file
|
||||||
|
[freqtrade/default_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/strategy/default_strategy.py).
|
||||||
|
|
||||||
|
### Further strategy ideas
|
||||||
|
|
||||||
|
To get additional Ideas for strategies, head over to our [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk.
|
||||||
|
Feel free to use any of them as inspiration for your own strategies.
|
||||||
|
We're happy to accept Pull Requests containing new Strategies to that repo.
|
||||||
|
|
||||||
|
We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) which is a great place to get and/or share ideas.
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
Now you have a perfect strategy you probably want to backtesting it.
|
|
||||||
Your next step is to learn [How to use the Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/backtesting.md).
|
Now you have a perfect strategy you probably want to backtest it.
|
||||||
|
Your next step is to learn [How to use the Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md).
|
||||||
|
@ -9,8 +9,9 @@ it.
|
|||||||
|
|
||||||
## Bot commands
|
## Bot commands
|
||||||
```
|
```
|
||||||
usage: main.py [-h] [-c PATH] [-v] [--version] [--dynamic-whitelist [INT]]
|
usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
|
||||||
[--dry-run-db]
|
[--strategy-path PATH] [--dynamic-whitelist [INT]]
|
||||||
|
[--db-url PATH]
|
||||||
{backtesting,hyperopt} ...
|
{backtesting,hyperopt} ...
|
||||||
|
|
||||||
Simple High Frequency Trading Bot for crypto currencies
|
Simple High Frequency Trading Bot for crypto currencies
|
||||||
@ -26,17 +27,17 @@ optional arguments:
|
|||||||
--version show program's version number and exit
|
--version show program's version number and exit
|
||||||
-c PATH, --config PATH
|
-c PATH, --config PATH
|
||||||
specify configuration file (default: config.json)
|
specify configuration file (default: config.json)
|
||||||
|
-d PATH, --datadir PATH
|
||||||
|
path to backtest data
|
||||||
-s NAME, --strategy NAME
|
-s NAME, --strategy NAME
|
||||||
specify strategy class name (default: DefaultStrategy)
|
specify strategy class name (default: DefaultStrategy)
|
||||||
--strategy-path PATH specify additional strategy lookup path
|
--strategy-path PATH specify additional strategy lookup path
|
||||||
--dry-run-db Force dry run to use a local DB
|
|
||||||
"tradesv3.dry_run.sqlite" instead of memory DB. Work
|
|
||||||
only if dry_run is enabled.
|
|
||||||
--datadir PATH
|
|
||||||
path to backtest data (default freqdata/tests/testdata
|
|
||||||
--dynamic-whitelist [INT]
|
--dynamic-whitelist [INT]
|
||||||
dynamically generate and update whitelist based on 24h
|
dynamically generate and update whitelist based on 24h
|
||||||
BaseVolume (Default 20 currencies)
|
BaseVolume (default: 20)
|
||||||
|
--db-url PATH Override trades database URL, this is useful if
|
||||||
|
dry_run is enabled or in custom deployments (default:
|
||||||
|
sqlite:///tradesv3.sqlite)
|
||||||
```
|
```
|
||||||
|
|
||||||
### How to use a different config file?
|
### How to use a different config file?
|
||||||
@ -66,7 +67,7 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy
|
|||||||
If the bot does not find your strategy file, it will display in an error
|
If the bot does not find your strategy file, it will display in an error
|
||||||
message the reason (File not found, or errors in your code).
|
message the reason (File not found, or errors in your code).
|
||||||
|
|
||||||
Learn more about strategy file in [optimize your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md).
|
Learn more about strategy file in [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).
|
||||||
|
|
||||||
### How to use --strategy-path?
|
### How to use --strategy-path?
|
||||||
This parameter allows you to add an additional strategy lookup path, which gets
|
This parameter allows you to add an additional strategy lookup path, which gets
|
||||||
@ -100,14 +101,14 @@ python3 ./freqtrade/main.py --dynamic-whitelist 30
|
|||||||
negative value (e.g -2), `--dynamic-whitelist` will use the default
|
negative value (e.g -2), `--dynamic-whitelist` will use the default
|
||||||
value (20).
|
value (20).
|
||||||
|
|
||||||
### How to use --dry-run-db?
|
### How to use --db-url?
|
||||||
When you run the bot in Dry-run mode, per default no transactions are
|
When you run the bot in Dry-run mode, per default no transactions are
|
||||||
stored in a database. If you want to store your bot actions in a DB
|
stored in a database. If you want to store your bot actions in a DB
|
||||||
using `--dry-run-db`. This command will use a separate database file
|
using `--db-url`. This can also be used to specify a custom database
|
||||||
`tradesv3.dry_run.sqlite`
|
in production mode. Example command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py -c config.json --dry-run-db
|
python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -116,21 +117,32 @@ python3 ./freqtrade/main.py -c config.json --dry-run-db
|
|||||||
Backtesting also uses the config specified via `-c/--config`.
|
Backtesting also uses the config specified via `-c/--config`.
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade backtesting [-h] [-l] [-i INT] [--realistic-simulation]
|
usage: main.py backtesting [-h] [-i TICKER_INTERVAL] [--realistic-simulation]
|
||||||
[-r]
|
[--timerange TIMERANGE] [-l] [-r] [--export EXPORT]
|
||||||
|
[--export-filename EXPORTFILENAME]
|
||||||
|
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-l, --live using live data
|
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
||||||
-i INT, --ticker-interval INT
|
specify ticker interval (1m, 5m, 30m, 1h, 1d)
|
||||||
specify ticker interval (default: '5m')
|
|
||||||
--realistic-simulation
|
--realistic-simulation
|
||||||
uses max_open_trades from config to simulate real
|
uses max_open_trades from config to simulate real
|
||||||
world limitations
|
world limitations
|
||||||
|
--timerange TIMERANGE
|
||||||
|
specify what timerange of data to use.
|
||||||
|
-l, --live using live data
|
||||||
-r, --refresh-pairs-cached
|
-r, --refresh-pairs-cached
|
||||||
refresh the pairs files in tests/testdata with
|
refresh the pairs files in tests/testdata with the
|
||||||
the latest data from the exchange. Use it if you want
|
latest data from the exchange. Use it if you want to
|
||||||
to run your backtesting with up-to-date data.
|
run your backtesting with up-to-date data.
|
||||||
|
--export EXPORT export backtest results, argument are: trades Example
|
||||||
|
--export=trades
|
||||||
|
--export-filename EXPORTFILENAME
|
||||||
|
Save backtest results to this filename requires
|
||||||
|
--export to be set as well Example --export-
|
||||||
|
filename=backtest_today.json (default: backtest-
|
||||||
|
result.json
|
||||||
```
|
```
|
||||||
|
|
||||||
### How to use --refresh-pairs-cached parameter?
|
### How to use --refresh-pairs-cached parameter?
|
||||||
@ -153,21 +165,32 @@ Hyperopt uses an internal json config return by `hyperopt_optimize_conf()`
|
|||||||
located in `freqtrade/optimize/hyperopt_conf.py`.
|
located in `freqtrade/optimize/hyperopt_conf.py`.
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade hyperopt [-h] [-e INT] [--use-mongodb]
|
usage: main.py hyperopt [-h] [-i TICKER_INTERVAL] [--realistic-simulation]
|
||||||
|
[--timerange TIMERANGE] [-e INT] [--use-mongodb]
|
||||||
|
[-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
||||||
|
specify ticker interval (1m, 5m, 30m, 1h, 1d)
|
||||||
|
--realistic-simulation
|
||||||
|
uses max_open_trades from config to simulate real
|
||||||
|
world limitations
|
||||||
|
--timerange TIMERANGE
|
||||||
|
specify what timerange of data to use.
|
||||||
-e INT, --epochs INT specify number of epochs (default: 100)
|
-e INT, --epochs INT specify number of epochs (default: 100)
|
||||||
--use-mongodb parallelize evaluations with mongodb (requires mongod
|
--use-mongodb parallelize evaluations with mongodb (requires mongod
|
||||||
in PATH)
|
in PATH)
|
||||||
|
-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...], --spaces {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]
|
||||||
|
Specify which parameters to hyperopt. Space separate
|
||||||
|
list. Default: all
|
||||||
```
|
```
|
||||||
|
|
||||||
## A parameter missing in the configuration?
|
## A parameter missing in the configuration?
|
||||||
All parameters for `main.py`, `backtesting`, `hyperopt` are referenced
|
All parameters for `main.py`, `backtesting`, `hyperopt` are referenced
|
||||||
in [misc.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/misc.py#L84)
|
in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L84)
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
The optimal strategy of the bot will change with time depending of the
|
The optimal strategy of the bot will change with time depending of the
|
||||||
market trends. The next step is to
|
market trends. The next step is to
|
||||||
[optimize your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md).
|
[optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).
|
||||||
|
@ -24,7 +24,7 @@ The table below will list all configuration parameters.
|
|||||||
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file.
|
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file.
|
||||||
| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled.
|
| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled.
|
||||||
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
|
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
|
||||||
| `exchange.name` | bittrex | Yes | Name of the exchange class to use.
|
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
|
||||||
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
|
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
|
||||||
| `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode.
|
| `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode.
|
||||||
| `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
|
| `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
|
||||||
@ -34,13 +34,14 @@ The table below will list all configuration parameters.
|
|||||||
| `telegram.enabled` | true | Yes | Enable or not the usage of Telegram.
|
| `telegram.enabled` | true | Yes | Enable or not the usage of Telegram.
|
||||||
| `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
|
| `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
|
||||||
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
|
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
|
||||||
|
| `db_url` | `sqlite:///tradesv3.sqlite` | No | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`.
|
||||||
| `initial_state` | running | No | Defines the initial application state. More information below.
|
| `initial_state` | running | No | Defines the initial application state. More information below.
|
||||||
| `strategy` | DefaultStrategy | No | Defines Strategy class to use.
|
| `strategy` | DefaultStrategy | No | Defines Strategy class to use.
|
||||||
| `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder).
|
| `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder).
|
||||||
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
|
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
|
||||||
|
|
||||||
The definition of each config parameters is in
|
The definition of each config parameters is in
|
||||||
[misc.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/misc.py#L205).
|
[misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205).
|
||||||
|
|
||||||
### Understand minimal_roi
|
### Understand minimal_roi
|
||||||
`minimal_roi` is a JSON object where the key is a duration
|
`minimal_roi` is a JSON object where the key is a duration
|
||||||
@ -73,15 +74,35 @@ value. This parameter is optional. If you use it, it will take over the
|
|||||||
Possible values are `running` or `stopped`. (default=`running`)
|
Possible values are `running` or `stopped`. (default=`running`)
|
||||||
If the value is `stopped` the bot has to be started with `/start` first.
|
If the value is `stopped` the bot has to be started with `/start` first.
|
||||||
|
|
||||||
|
### Understand process_throttle_secs
|
||||||
|
`process_throttle_secs` is an optional field that defines in seconds how long the bot should wait
|
||||||
|
before asking the strategy if we should buy or a sell an asset. After each wait period, the strategy is asked again for
|
||||||
|
every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or
|
||||||
|
the static list of pairs) if we should buy.
|
||||||
|
|
||||||
### Understand ask_last_balance
|
### Understand ask_last_balance
|
||||||
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will
|
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will
|
||||||
use the `last` price and values between those interpolate between ask and last
|
use the `last` price and values between those interpolate between ask and last
|
||||||
price. Using `ask` price will guarantee quick success in bid, but bot will also
|
price. Using `ask` price will guarantee quick success in bid, but bot will also
|
||||||
end up paying more then would probably have been necessary.
|
end up paying more then would probably have been necessary.
|
||||||
|
|
||||||
|
### What values for exchange.name?
|
||||||
|
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency
|
||||||
|
exchange markets and trading APIs. The complete up-to-date list can be found in the
|
||||||
|
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested
|
||||||
|
with only Bittrex and Binance.
|
||||||
|
|
||||||
|
The bot was tested with the following exchanges:
|
||||||
|
- [Bittrex](https://bittrex.com/): "bittrex"
|
||||||
|
- [Binance](https://www.binance.com/): "binance"
|
||||||
|
|
||||||
|
Feel free to test other exchanges and submit your PR to improve the bot.
|
||||||
|
|
||||||
### What values for fiat_display_currency?
|
### What values for fiat_display_currency?
|
||||||
`fiat_display_currency` set the fiat to use for the conversion form coin to fiat in Telegram.
|
`fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram.
|
||||||
The valid value are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD".
|
The valid values are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD".
|
||||||
|
In addition to central bank currencies, a range of cryto currencies are supported.
|
||||||
|
The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT".
|
||||||
|
|
||||||
## Switch to dry-run mode
|
## Switch to dry-run mode
|
||||||
We recommend starting the bot in dry-run mode to see how your bot will
|
We recommend starting the bot in dry-run mode to see how your bot will
|
||||||
@ -91,12 +112,13 @@ creating trades.
|
|||||||
|
|
||||||
### To switch your bot in Dry-run mode:
|
### To switch your bot in Dry-run mode:
|
||||||
1. Edit your `config.json` file
|
1. Edit your `config.json` file
|
||||||
2. Switch dry-run to true
|
2. Switch dry-run to true and specify db_url for a persistent db
|
||||||
```json
|
```json
|
||||||
"dry_run": true,
|
"dry_run": true,
|
||||||
|
"db_url": "sqlite///tradesv3.dryrun.sqlite",
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Remove your Bittrex API key (change them by fake api credentials)
|
3. Remove your Exchange API key (change them by fake api credentials)
|
||||||
```json
|
```json
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
@ -117,12 +139,12 @@ you run it in production mode.
|
|||||||
### To switch your bot in production mode:
|
### To switch your bot in production mode:
|
||||||
1. Edit your `config.json` file
|
1. Edit your `config.json` file
|
||||||
|
|
||||||
2. Switch dry-run to false
|
2. Switch dry-run to false and don't forget to adapt your database URL if set
|
||||||
```json
|
```json
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Insert your Bittrex API key (change them by fake api keys)
|
3. Insert your Exchange API key (change them by fake api keys)
|
||||||
```json
|
```json
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
@ -132,9 +154,8 @@ you run it in production mode.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
If you have not your Bittrex API key yet,
|
If you have not your Bittrex API key yet,
|
||||||
[see our tutorial](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md).
|
[see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md).
|
||||||
|
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
Now you have configured your config.json, the next step is to
|
Now you have configured your config.json, the next step is to
|
||||||
[start your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md).
|
[start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md).
|
||||||
|
@ -27,7 +27,7 @@ like pauses. You can stop your bot, adjust settings and start it again.
|
|||||||
#### I want to improve the bot with a new strategy
|
#### I want to improve the bot with a new strategy
|
||||||
|
|
||||||
That's great. We have a nice backtesting and hyperoptimizing setup. See
|
That's great. We have a nice backtesting and hyperoptimizing setup. See
|
||||||
the tutorial [here|Testing-new-strategies-with-Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands).
|
the tutorial [here|Testing-new-strategies-with-Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands).
|
||||||
|
|
||||||
#### Is there a setting to only SELL the coins being held and not
|
#### Is there a setting to only SELL the coins being held and not
|
||||||
perform anymore BUYS?
|
perform anymore BUYS?
|
||||||
|
@ -14,13 +14,13 @@ parameters with Hyperopt.
|
|||||||
|
|
||||||
## Prepare Hyperopt
|
## Prepare Hyperopt
|
||||||
Before we start digging in Hyperopt, we recommend you to take a look at
|
Before we start digging in Hyperopt, we recommend you to take a look at
|
||||||
your strategy file located into [user_data/strategies/](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
your strategy file located into [user_data/strategies/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
||||||
|
|
||||||
### 1. Configure your Guards and Triggers
|
### 1. Configure your Guards and Triggers
|
||||||
There are two places you need to change in your strategy file to add a
|
There are two places you need to change in your strategy file to add a
|
||||||
new buy strategy for testing:
|
new buy strategy for testing:
|
||||||
- Inside [populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L278-L294).
|
- Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L278-L294).
|
||||||
- Inside [hyperopt_space()](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297) known as `SPACE`.
|
- Inside [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297) known as `SPACE`.
|
||||||
|
|
||||||
There you have two different type of indicators: 1. `guards` and 2.
|
There you have two different type of indicators: 1. `guards` and 2.
|
||||||
`triggers`.
|
`triggers`.
|
||||||
@ -110,13 +110,13 @@ cannot use your config file. It is also made on purpose to allow you
|
|||||||
testing your strategy with different configurations.
|
testing your strategy with different configurations.
|
||||||
|
|
||||||
The Hyperopt configuration is located in
|
The Hyperopt configuration is located in
|
||||||
[user_data/hyperopt_conf.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/hyperopt_conf.py).
|
[user_data/hyperopt_conf.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopt_conf.py).
|
||||||
|
|
||||||
|
|
||||||
## Advanced notions
|
## Advanced notions
|
||||||
### Understand the Guards and Triggers
|
### Understand the Guards and Triggers
|
||||||
When you need to add the new guards and triggers to be hyperopt
|
When you need to add the new guards and triggers to be hyperopt
|
||||||
parameters, you do this by adding them into the [hyperopt_space()](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297).
|
parameters, you do this by adding them into the [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297).
|
||||||
|
|
||||||
If it's a trigger, you add one line to the 'trigger' choice group and that's it.
|
If it's a trigger, you add one line to the 'trigger' choice group and that's it.
|
||||||
|
|
||||||
@ -312,4 +312,4 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
|||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
Now you have a perfect bot and want to control it from Telegram. Your
|
Now you have a perfect bot and want to control it from Telegram. Your
|
||||||
next step is to learn the [Telegram usage](https://github.com/gcarq/freqtrade/blob/develop/docs/telegram-usage.md).
|
next step is to learn the [Telegram usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md).
|
||||||
|
@ -6,27 +6,27 @@ Pull-request. Do not hesitate to reach us on
|
|||||||
if you do not find the answer to your questions.
|
if you do not find the answer to your questions.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [Pre-requisite](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md)
|
- [Pre-requisite](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md)
|
||||||
- [Setup your Bittrex account](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account)
|
- [Setup your Bittrex account](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account)
|
||||||
- [Setup your Telegram bot](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot)
|
- [Setup your Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot)
|
||||||
- [Bot Installation](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md)
|
- [Bot Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
|
||||||
- [Install with Docker (all platforms)](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md#docker)
|
- [Install with Docker (all platforms)](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#docker)
|
||||||
- [Install on Linux Ubuntu](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md#21-linux---ubuntu-1604)
|
- [Install on Linux Ubuntu](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#21-linux---ubuntu-1604)
|
||||||
- [Install on MacOS](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md#23-macos-installation)
|
- [Install on MacOS](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#23-macos-installation)
|
||||||
- [Install on Windows](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md#windows)
|
- [Install on Windows](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#windows)
|
||||||
- [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)
|
- [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
|
||||||
- [Bot usage (Start your bot)](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md)
|
- [Bot usage (Start your bot)](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md)
|
||||||
- [Bot commands](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
|
- [Bot commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
|
||||||
- [Backtesting commands](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
|
- [Backtesting commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
|
||||||
- [Hyperopt commands](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
|
- [Hyperopt commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
|
||||||
- [Bot Optimization](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md)
|
- [Bot Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
|
||||||
- [Change your strategy](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md#change-your-strategy)
|
- [Change your strategy](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#change-your-strategy)
|
||||||
- [Add more Indicator](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md#add-more-indicator)
|
- [Add more Indicator](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#add-more-indicator)
|
||||||
- [Test your strategy with Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/backtesting.md)
|
- [Test your strategy with Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
||||||
- [Find optimal parameters with Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/hyperopt.md)
|
- [Find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||||
- [Control the bot with telegram](https://github.com/gcarq/freqtrade/blob/develop/docs/telegram-usage.md)
|
- [Control the bot with telegram](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md)
|
||||||
- [Contribute to the project](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
|
- [Contribute to the project](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
- [How to contribute](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
|
- [How to contribute](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
- [Run tests & Check PEP8 compliance](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
|
- [Run tests & Check PEP8 compliance](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
- [FAQ](https://github.com/gcarq/freqtrade/blob/develop/docs/faq.md)
|
- [FAQ](https://github.com/freqtrade/freqtrade/blob/develop/docs/faq.md)
|
||||||
- [SQL cheatsheet](https://github.com/gcarq/freqtrade/blob/develop/docs/sql_cheatsheet.md)
|
- [SQL cheatsheet](https://github.com/freqtrade/freqtrade/blob/develop/docs/sql_cheatsheet.md)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
This page explains how to prepare your environment for running the bot.
|
This page explains how to prepare your environment for running the bot.
|
||||||
|
|
||||||
To understand how to set up the bot please read the [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md) page.
|
To understand how to set up the bot please read the [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) page.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
@ -16,7 +16,6 @@ To understand how to set up the bot please read the [Bot Configuration](https://
|
|||||||
- [Setup Config and virtual env](#setup-config-and-virtual-env)
|
- [Setup Config and virtual env](#setup-config-and-virtual-env)
|
||||||
* [Windows](#windows)
|
* [Windows](#windows)
|
||||||
|
|
||||||
|
|
||||||
<!-- /TOC -->
|
<!-- /TOC -->
|
||||||
|
|
||||||
------
|
------
|
||||||
@ -35,7 +34,9 @@ usage:
|
|||||||
```
|
```
|
||||||
|
|
||||||
### --install
|
### --install
|
||||||
|
|
||||||
This script will install everything you need to run the bot:
|
This script will install everything you need to run the bot:
|
||||||
|
|
||||||
* Mandatory software as: `Python3`, `ta-lib`, `wget`
|
* Mandatory software as: `Python3`, `ta-lib`, `wget`
|
||||||
* Setup your virtualenv
|
* Setup your virtualenv
|
||||||
* Configure your `config.json` file
|
* Configure your `config.json` file
|
||||||
@ -43,12 +44,15 @@ This script will install everything you need to run the bot:
|
|||||||
This script is a combination of `install script` `--reset`, `--config`
|
This script is a combination of `install script` `--reset`, `--config`
|
||||||
|
|
||||||
### --update
|
### --update
|
||||||
|
|
||||||
Update parameter will pull the last version of your current branch and update your virtualenv.
|
Update parameter will pull the last version of your current branch and update your virtualenv.
|
||||||
|
|
||||||
### --reset
|
### --reset
|
||||||
|
|
||||||
Reset parameter will hard reset your branch (only if you are on `master` or `develop`) and recreate your virtualenv.
|
Reset parameter will hard reset your branch (only if you are on `master` or `develop`) and recreate your virtualenv.
|
||||||
|
|
||||||
### --config
|
### --config
|
||||||
|
|
||||||
Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`.
|
Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`.
|
||||||
|
|
||||||
------
|
------
|
||||||
@ -63,13 +67,12 @@ Start by downloading Docker for your platform:
|
|||||||
|
|
||||||
Once you have Docker installed, simply create the config file (e.g. `config.json`) and then create a Docker image for `freqtrade` using the Dockerfile in this repo.
|
Once you have Docker installed, simply create the config file (e.g. `config.json`) and then create a Docker image for `freqtrade` using the Dockerfile in this repo.
|
||||||
|
|
||||||
|
|
||||||
### 1. Prepare the Bot
|
### 1. Prepare the Bot
|
||||||
|
|
||||||
#### 1.1. Clone the git repository
|
#### 1.1. Clone the git repository
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/gcarq/freqtrade.git
|
git clone https://github.com/freqtrade/freqtrade.git
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 1.2. (Optional) Checkout the develop branch
|
#### 1.2. (Optional) Checkout the develop branch
|
||||||
@ -90,21 +93,22 @@ cd freqtrade
|
|||||||
cp -n config.json.example config.json
|
cp -n config.json.example config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
> To edit the config please refer to the [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md) page.
|
> To edit the config please refer to the [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) page.
|
||||||
|
|
||||||
#### 1.5. Create your database file *(optional - the bot will create it if it is missing)*
|
#### 1.5. Create your database file *(optional - the bot will create it if it is missing)*
|
||||||
|
|
||||||
Production
|
Production
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
touch tradesv3.sqlite
|
touch tradesv3.sqlite
|
||||||
````
|
````
|
||||||
|
|
||||||
Dry-Run
|
Dry-Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
touch tradesv3.dryrun.sqlite
|
touch tradesv3.dryrun.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### 2. Build the Docker image
|
### 2. Build the Docker image
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -114,7 +118,6 @@ docker build -t freqtrade .
|
|||||||
|
|
||||||
For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see the "5. Run a restartable docker image" section) to keep it between updates.
|
For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see the "5. Run a restartable docker image" section) to keep it between updates.
|
||||||
|
|
||||||
|
|
||||||
### 3. Verify the Docker image
|
### 3. Verify the Docker image
|
||||||
|
|
||||||
After the build process you can verify that the image was created with:
|
After the build process you can verify that the image was created with:
|
||||||
@ -123,7 +126,6 @@ After the build process you can verify that the image was created with:
|
|||||||
docker images
|
docker images
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### 4. Run the Docker image
|
### 4. Run the Docker image
|
||||||
|
|
||||||
You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory):
|
You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory):
|
||||||
@ -132,8 +134,15 @@ You can run a one-off container that is immediately deleted upon exiting with th
|
|||||||
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
In this example, the database will be created inside the docker instance and will be lost when you will refresh your image.
|
There is known issue in OSX Docker versions after 17.09.1, whereby /etc/localtime cannot be shared causing Docker to not start. A work-around for this is to start with the following cmd.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
||||||
|
```
|
||||||
|
|
||||||
|
More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396)
|
||||||
|
|
||||||
|
In this example, the database will be created inside the docker instance and will be lost when you will refresh your image.
|
||||||
|
|
||||||
### 5. Run a restartable docker image
|
### 5. Run a restartable docker image
|
||||||
|
|
||||||
@ -155,10 +164,11 @@ docker run -d \
|
|||||||
-v /etc/localtime:/etc/localtime:ro \
|
-v /etc/localtime:/etc/localtime:ro \
|
||||||
-v ~/.freqtrade/config.json:/freqtrade/config.json \
|
-v ~/.freqtrade/config.json:/freqtrade/config.json \
|
||||||
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
||||||
freqtrade
|
freqtrade --db-url sqlite:///tradesv3.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
If you are using `dry_run=True` it's not necessary to mount `tradesv3.sqlite`, but you can mount `tradesv3.dryrun.sqlite` if you plan to use the dry run mode with the param `--dry-run-db`.
|
NOTE: db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used.
|
||||||
|
To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite`
|
||||||
|
|
||||||
### 6. Monitor your Docker instance
|
### 6. Monitor your Docker instance
|
||||||
|
|
||||||
@ -183,13 +193,13 @@ We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windo
|
|||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
Click each one for install guide:
|
Click each one for install guide:
|
||||||
|
|
||||||
* [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/), note the bot was not tested on Python >= 3.7.x
|
* [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/), note the bot was not tested on Python >= 3.7.x
|
||||||
* [pip](https://pip.pypa.io/en/stable/installing/)
|
* [pip](https://pip.pypa.io/en/stable/installing/)
|
||||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
|
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
|
||||||
* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
||||||
|
|
||||||
|
|
||||||
### Linux - Ubuntu 16.04
|
### Linux - Ubuntu 16.04
|
||||||
|
|
||||||
#### 1. Install Python 3.6, Git, and wget
|
#### 1. Install Python 3.6, Git, and wget
|
||||||
@ -230,13 +240,7 @@ sudo apt-get install mongodb-org
|
|||||||
Clone the git repository:
|
Clone the git repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/gcarq/freqtrade.git
|
git clone https://github.com/freqtrade/freqtrade.git
|
||||||
```
|
|
||||||
|
|
||||||
Optionally checkout the develop branch:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git checkout develop
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 5. Configure `freqtrade` as a `systemd` service
|
#### 5. Configure `freqtrade` as a `systemd` service
|
||||||
@ -244,6 +248,7 @@ git checkout develop
|
|||||||
From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
|
From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
|
||||||
|
|
||||||
After that you can start the daemon with:
|
After that you can start the daemon with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
systemctl --user start freqtrade
|
systemctl --user start freqtrade
|
||||||
```
|
```
|
||||||
@ -254,7 +259,6 @@ For this to be persistent (run when user is logged out) you'll need to enable `l
|
|||||||
sudo loginctl enable-linger "$USER"
|
sudo loginctl enable-linger "$USER"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### MacOS
|
### MacOS
|
||||||
|
|
||||||
#### 1. Install Python 3.6, git, wget and ta-lib
|
#### 1. Install Python 3.6, git, wget and ta-lib
|
||||||
@ -280,7 +284,7 @@ export PATH=<path_freqtrade>/env/mongodb/bin:$PATH
|
|||||||
Clone the git repository:
|
Clone the git repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/gcarq/freqtrade.git
|
git clone https://github.com/freqtrade/freqtrade.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Optionally checkout the develop branch:
|
Optionally checkout the develop branch:
|
||||||
@ -289,7 +293,6 @@ Optionally checkout the develop branch:
|
|||||||
git checkout develop
|
git checkout develop
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Setup Config and virtual env
|
### Setup Config and virtual env
|
||||||
|
|
||||||
#### 1. Initialize the configuration
|
#### 1. Initialize the configuration
|
||||||
@ -299,8 +302,7 @@ cd freqtrade
|
|||||||
cp config.json.example config.json
|
cp config.json.example config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
> *To edit the config please refer to [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md).*
|
> *To edit the config please refer to [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md).*
|
||||||
|
|
||||||
|
|
||||||
#### 2. Setup your Python virtual environment (virtualenv)
|
#### 2. Setup your Python virtual environment (virtualenv)
|
||||||
|
|
||||||
@ -324,27 +326,41 @@ python3.6 ./freqtrade/main.py -c config.json
|
|||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
|
|
||||||
We recommend that Windows users use [Docker](#docker) as this will work
|
We recommend that Windows users use [Docker](#docker) as this will work much easier and smoother (also more secure).
|
||||||
much easier and smoother (also more secure).
|
|
||||||
|
|
||||||
### Install freqtrade
|
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
|
||||||
|
If that is not available on your system, feel free to try the instructions below, which led to success for some.
|
||||||
|
|
||||||
|
### Install freqtrade manually
|
||||||
|
|
||||||
|
#### Clone the git repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/freqtrade/freqtrade.git
|
||||||
|
```
|
||||||
|
|
||||||
copy paste `config.json` to ``\path\freqtrade-develop\freqtrade`
|
copy paste `config.json` to ``\path\freqtrade-develop\freqtrade`
|
||||||
|
|
||||||
|
#### install ta-lib
|
||||||
|
|
||||||
|
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
||||||
|
|
||||||
|
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of inofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib‑0.4.17‑cp36‑cp36m‑win32.whl` (make sure to use the version matching your python version)
|
||||||
|
|
||||||
```cmd
|
```cmd
|
||||||
>cd \path\freqtrade-develop
|
>cd \path\freqtrade-develop
|
||||||
>python -m venv .env
|
>python -m venv .env
|
||||||
>cd .env\Scripts
|
>cd .env\Scripts
|
||||||
>activate.bat
|
>activate.bat
|
||||||
>cd \path\freqtrade-develop
|
>cd \path\freqtrade-develop
|
||||||
|
REM optionally install ta-lib from wheel
|
||||||
|
REM >pip install TA_Lib‑0.4.17‑cp36‑cp36m‑win32.whl
|
||||||
>pip install -r requirements.txt
|
>pip install -r requirements.txt
|
||||||
>pip install -e .
|
>pip install -e .
|
||||||
>cd freqtrade
|
>python freqtrade\main.py
|
||||||
>python main.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/gcarq/freqtrade/issues/222)
|
> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222)
|
||||||
|
|
||||||
|
|
||||||
Now you have an environment ready, the next step is
|
Now you have an environment ready, the next step is
|
||||||
[Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)...
|
[Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)...
|
||||||
|
@ -48,6 +48,12 @@ To plot trades stored in a database use `--db-url` argument:
|
|||||||
python scripts/plot_dataframe.py --db-url tradesv3.dry_run.sqlite -p BTC_ETH
|
python scripts/plot_dataframe.py --db-url tradesv3.dry_run.sqlite -p BTC_ETH
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To plot a test strategy the strategy should have first be backtested.
|
||||||
|
The results may then be plotted with the -s argument:
|
||||||
|
```
|
||||||
|
python scripts/plot_dataframe.py -s Strategy_Name -p BTC/ETH --datadir user_data/data/<exchange_name>/
|
||||||
|
```
|
||||||
|
|
||||||
## Plot profit
|
## Plot profit
|
||||||
|
|
||||||
The profit plotter show a picture with three plots:
|
The profit plotter show a picture with three plots:
|
||||||
|
@ -85,7 +85,7 @@ INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, sta
|
|||||||
|
|
||||||
## Fix wrong fees in the table
|
## Fix wrong fees in the table
|
||||||
If your DB was created before
|
If your DB was created before
|
||||||
[PR#200](https://github.com/gcarq/freqtrade/pull/200) was merged
|
[PR#200](https://github.com/freqtrade/freqtrade/pull/200) was merged
|
||||||
(before 12/23/17).
|
(before 12/23/17).
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
|
@ -4,7 +4,7 @@ This page explains how to command your bot with Telegram.
|
|||||||
|
|
||||||
## Pre-requisite
|
## Pre-requisite
|
||||||
To control your bot with Telegram, you need first to
|
To control your bot with Telegram, you need first to
|
||||||
[set up a Telegram bot](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md)
|
[set up a Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md)
|
||||||
and add your Telegram API keys into your config file.
|
and add your Telegram API keys into your config file.
|
||||||
|
|
||||||
## Telegram commands
|
## Telegram commands
|
||||||
|
@ -12,7 +12,8 @@ class DependencyException(BaseException):
|
|||||||
class OperationalException(BaseException):
|
class OperationalException(BaseException):
|
||||||
"""
|
"""
|
||||||
Requires manual intervention.
|
Requires manual intervention.
|
||||||
This happens when an exchange returns an unexpected error during runtime.
|
This happens when an exchange returns an unexpected error during runtime
|
||||||
|
or given configuration is invalid.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
15
freqtrade/__main__.py
Normal file
15
freqtrade/__main__.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
__main__.py for Freqtrade
|
||||||
|
To launch Freqtrade as a module
|
||||||
|
|
||||||
|
> python -m freqtrade (with Python >= 3.6)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from freqtrade import main
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main.set_loggers()
|
||||||
|
main.main(sys.argv[1:])
|
@ -12,7 +12,7 @@ from pandas import DataFrame, to_datetime
|
|||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.exchange import get_ticker_history
|
from freqtrade.exchange import get_ticker_history
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.strategy.resolver import StrategyResolver
|
from freqtrade.strategy.resolver import StrategyResolver, IStrategy
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -37,7 +37,7 @@ class Analyze(object):
|
|||||||
:param config: Bot configuration (use the one from Configuration())
|
:param config: Bot configuration (use the one from Configuration())
|
||||||
"""
|
"""
|
||||||
self.config = config
|
self.config = config
|
||||||
self.strategy = StrategyResolver(self.config).strategy
|
self.strategy: IStrategy = StrategyResolver(self.config).strategy
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||||
@ -62,6 +62,7 @@ class Analyze(object):
|
|||||||
'close': 'last',
|
'close': 'last',
|
||||||
'volume': 'max',
|
'volume': 'max',
|
||||||
})
|
})
|
||||||
|
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
@ -2,24 +2,36 @@
|
|||||||
This module contains the argument manager class
|
This module contains the argument manager class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import arrow
|
import arrow
|
||||||
from typing import List, Tuple, Optional
|
from typing import List, Optional, NamedTuple
|
||||||
|
|
||||||
from freqtrade import __version__, constants
|
from freqtrade import __version__, constants
|
||||||
|
|
||||||
|
|
||||||
|
class TimeRange(NamedTuple):
|
||||||
|
"""
|
||||||
|
NamedTuple Defining timerange inputs.
|
||||||
|
[start/stop]type defines if [start/stop]ts shall be used.
|
||||||
|
if *type is none, don't use corresponding startvalue.
|
||||||
|
"""
|
||||||
|
starttype: Optional[str] = None
|
||||||
|
stoptype: Optional[str] = None
|
||||||
|
startts: int = 0
|
||||||
|
stopts: int = 0
|
||||||
|
|
||||||
|
|
||||||
class Arguments(object):
|
class Arguments(object):
|
||||||
"""
|
"""
|
||||||
Arguments Class. Manage the arguments received by the cli
|
Arguments Class. Manage the arguments received by the cli
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args: List[str], description: str):
|
def __init__(self, args: List[str], description: str) -> None:
|
||||||
self.args = args
|
self.args = args
|
||||||
self.parsed_arg = None
|
self.parsed_arg: Optional[argparse.Namespace] = None
|
||||||
self.parser = argparse.ArgumentParser(description=description)
|
self.parser = argparse.ArgumentParser(description=description)
|
||||||
|
|
||||||
def _load_args(self) -> None:
|
def _load_args(self) -> None:
|
||||||
@ -60,7 +72,7 @@ class Arguments(object):
|
|||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'--version',
|
'--version',
|
||||||
action='version',
|
action='version',
|
||||||
version='%(prog)s {}'.format(__version__),
|
version=f'%(prog)s {__version__}'
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'-c', '--config',
|
'-c', '--config',
|
||||||
@ -72,9 +84,9 @@ class Arguments(object):
|
|||||||
)
|
)
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'-d', '--datadir',
|
'-d', '--datadir',
|
||||||
help='path to backtest data (default: %(default)s',
|
help='path to backtest data',
|
||||||
dest='datadir',
|
dest='datadir',
|
||||||
default=os.path.join('freqtrade', 'tests', 'testdata'),
|
default=None,
|
||||||
type=str,
|
type=str,
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
)
|
)
|
||||||
@ -95,8 +107,8 @@ class Arguments(object):
|
|||||||
)
|
)
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'--dynamic-whitelist',
|
'--dynamic-whitelist',
|
||||||
help='dynamically generate and update whitelist \
|
help='dynamically generate and update whitelist'
|
||||||
based on 24h BaseVolume (Default 20 currencies)', # noqa
|
' based on 24h BaseVolume (default: %(const)s)',
|
||||||
dest='dynamic_whitelist',
|
dest='dynamic_whitelist',
|
||||||
const=constants.DYNAMIC_WHITELIST,
|
const=constants.DYNAMIC_WHITELIST,
|
||||||
type=int,
|
type=int,
|
||||||
@ -104,11 +116,13 @@ class Arguments(object):
|
|||||||
nargs='?',
|
nargs='?',
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'--dry-run-db',
|
'--db-url',
|
||||||
help='Force dry run to use a local DB "tradesv3.dry_run.sqlite" \
|
help='Override trades database URL, this is useful if dry_run is enabled'
|
||||||
instead of memory DB. Work only if dry_run is enabled.',
|
' or in custom deployments (default: %(default)s)',
|
||||||
action='store_true',
|
dest='db_url',
|
||||||
dest='dry_run_db',
|
default=constants.DEFAULT_DB_PROD_URL,
|
||||||
|
type=str,
|
||||||
|
metavar='PATH',
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -124,8 +138,8 @@ class Arguments(object):
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-r', '--refresh-pairs-cached',
|
'-r', '--refresh-pairs-cached',
|
||||||
help='refresh the pairs files in tests/testdata with the latest data from the exchange. \
|
help='refresh the pairs files in tests/testdata with the latest data from the '
|
||||||
Use it if you want to run your backtesting with up-to-date data.',
|
'exchange. Use it if you want to run your backtesting with up-to-date data.',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
dest='refresh_pairs',
|
dest='refresh_pairs',
|
||||||
)
|
)
|
||||||
@ -137,9 +151,25 @@ class Arguments(object):
|
|||||||
default=None,
|
default=None,
|
||||||
dest='export',
|
dest='export',
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--export-filename',
|
||||||
|
help='Save backtest results to this filename \
|
||||||
|
requires --export to be set as well\
|
||||||
|
Example --export-filename=user_data/backtest_data/backtest_today.json\
|
||||||
|
(default: %(default)s)',
|
||||||
|
type=str,
|
||||||
|
default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'),
|
||||||
|
dest='exportfilename',
|
||||||
|
metavar='PATH',
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
|
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""
|
||||||
|
Parses given common arguments for Backtesting and Hyperopt scripts.
|
||||||
|
:param parser:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-i', '--ticker-interval',
|
'-i', '--ticker-interval',
|
||||||
help='specify ticker interval (1m, 5m, 30m, 1h, 1d)',
|
help='specify ticker interval (1m, 5m, 30m, 1h, 1d)',
|
||||||
@ -211,17 +241,20 @@ class Arguments(object):
|
|||||||
self.hyperopt_options(hyperopt_cmd)
|
self.hyperopt_options(hyperopt_cmd)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_timerange(text: str) -> Optional[Tuple[List, int, int]]:
|
def parse_timerange(text: Optional[str]) -> TimeRange:
|
||||||
"""
|
"""
|
||||||
Parse the value of the argument --timerange to determine what is the range desired
|
Parse the value of the argument --timerange to determine what is the range desired
|
||||||
:param text: value from --timerange
|
:param text: value from --timerange
|
||||||
:return: Start and End range period
|
:return: Start and End range period
|
||||||
"""
|
"""
|
||||||
if text is None:
|
if text is None:
|
||||||
return None
|
return TimeRange(None, None, 0, 0)
|
||||||
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
||||||
(r'^(\d{8})-$', ('date', None)),
|
(r'^(\d{8})-$', ('date', None)),
|
||||||
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
|
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
|
||||||
|
(r'^-(\d{10})$', (None, 'date')),
|
||||||
|
(r'^(\d{10})-$', ('date', None)),
|
||||||
|
(r'^(\d{10})-(\d{10})$', ('date', 'date')),
|
||||||
(r'^(-\d+)$', (None, 'line')),
|
(r'^(-\d+)$', (None, 'line')),
|
||||||
(r'^(\d+)-$', ('line', None)),
|
(r'^(\d+)-$', ('line', None)),
|
||||||
(r'^(\d+)-(\d+)$', ('index', 'index'))]
|
(r'^(\d+)-(\d+)$', ('index', 'index'))]
|
||||||
@ -231,22 +264,24 @@ class Arguments(object):
|
|||||||
if match: # Regex has matched
|
if match: # Regex has matched
|
||||||
rvals = match.groups()
|
rvals = match.groups()
|
||||||
index = 0
|
index = 0
|
||||||
start = None
|
start: int = 0
|
||||||
stop = None
|
stop: int = 0
|
||||||
if stype[0]:
|
if stype[0]:
|
||||||
start = rvals[index]
|
starts = rvals[index]
|
||||||
if stype[0] == 'date':
|
if stype[0] == 'date':
|
||||||
start = arrow.get(start, 'YYYYMMDD').timestamp
|
start = int(starts) if len(starts) == 10 \
|
||||||
|
else arrow.get(starts, 'YYYYMMDD').timestamp
|
||||||
else:
|
else:
|
||||||
start = int(start)
|
start = int(starts)
|
||||||
index += 1
|
index += 1
|
||||||
if stype[1]:
|
if stype[1]:
|
||||||
stop = rvals[index]
|
stops = rvals[index]
|
||||||
if stype[1] == 'date':
|
if stype[1] == 'date':
|
||||||
stop = arrow.get(stop, 'YYYYMMDD').timestamp
|
stop = int(stops) if len(stops) == 10 \
|
||||||
|
else arrow.get(stops, 'YYYYMMDD').timestamp
|
||||||
else:
|
else:
|
||||||
stop = int(stop)
|
stop = int(stops)
|
||||||
return stype, start, stop
|
return TimeRange(stype[0], stype[1], start, stop)
|
||||||
raise Exception('Incorrect syntax for timerange "%s"' % text)
|
raise Exception('Incorrect syntax for timerange "%s"' % text)
|
||||||
|
|
||||||
def scripts_options(self) -> None:
|
def scripts_options(self) -> None:
|
||||||
@ -260,13 +295,6 @@ class Arguments(object):
|
|||||||
default=None
|
default=None
|
||||||
)
|
)
|
||||||
|
|
||||||
self.parser.add_argument(
|
|
||||||
'-db', '--db-url',
|
|
||||||
help='Show trades stored in database.',
|
|
||||||
dest='db_url',
|
|
||||||
default=None
|
|
||||||
)
|
|
||||||
|
|
||||||
def testdata_dl_options(self) -> None:
|
def testdata_dl_options(self) -> None:
|
||||||
"""
|
"""
|
||||||
Parses given arguments for testdata download
|
Parses given arguments for testdata download
|
||||||
@ -275,25 +303,42 @@ class Arguments(object):
|
|||||||
'--pairs-file',
|
'--pairs-file',
|
||||||
help='File containing a list of pairs to download',
|
help='File containing a list of pairs to download',
|
||||||
dest='pairs_file',
|
dest='pairs_file',
|
||||||
default=None
|
default=None,
|
||||||
|
metavar='PATH',
|
||||||
)
|
)
|
||||||
|
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'--export',
|
'--export',
|
||||||
help='Export files to given dir',
|
help='Export files to given dir',
|
||||||
dest='export',
|
dest='export',
|
||||||
default=None)
|
default=None,
|
||||||
|
metavar='PATH',
|
||||||
|
)
|
||||||
|
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'--days',
|
'--days',
|
||||||
help='Download data for number of days',
|
help='Download data for number of days',
|
||||||
dest='days',
|
dest='days',
|
||||||
type=int,
|
type=int,
|
||||||
default=None)
|
metavar='INT',
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'--exchange',
|
'--exchange',
|
||||||
help='Exchange name',
|
help='Exchange name (default: %(default)s)',
|
||||||
dest='exchange',
|
dest='exchange',
|
||||||
type=str,
|
type=str,
|
||||||
default='bittrex')
|
default='bittrex'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.parser.add_argument(
|
||||||
|
'-t', '--timeframes',
|
||||||
|
help='Specify which tickers to download. Space separated list. \
|
||||||
|
Default: %(default)s',
|
||||||
|
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
|
||||||
|
'6h', '8h', '12h', '1d', '3d', '1w'],
|
||||||
|
default=['1m', '5m'],
|
||||||
|
nargs='+',
|
||||||
|
dest='timeframes',
|
||||||
|
)
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
This module contains the configuration class
|
This module contains the configuration class
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from typing import Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
from jsonschema import Draft4Validator, validate
|
from jsonschema import Draft4Validator, validate
|
||||||
from jsonschema.exceptions import ValidationError, best_match
|
from jsonschema.exceptions import ValidationError, best_match
|
||||||
import ccxt
|
import ccxt
|
||||||
@ -23,7 +23,7 @@ class Configuration(object):
|
|||||||
"""
|
"""
|
||||||
def __init__(self, args: Namespace) -> None:
|
def __init__(self, args: Namespace) -> None:
|
||||||
self.args = args
|
self.args = args
|
||||||
self.config = None
|
self.config: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
def load_config(self) -> Dict[str, Any]:
|
def load_config(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@ -61,11 +61,9 @@ class Configuration(object):
|
|||||||
with open(path) as file:
|
with open(path) as file:
|
||||||
conf = json.load(file)
|
conf = json.load(file)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.critical(
|
raise OperationalException(
|
||||||
'Config file "%s" not found. Please create your config file',
|
'Config file "{}" not found!'
|
||||||
path
|
' Please create a config file or check whether it exists.'.format(path))
|
||||||
)
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
if 'internals' not in conf:
|
if 'internals' not in conf:
|
||||||
conf['internals'] = {}
|
conf['internals'] = {}
|
||||||
@ -97,22 +95,35 @@ class Configuration(object):
|
|||||||
'(not applicable with Backtesting and Hyperopt)'
|
'(not applicable with Backtesting and Hyperopt)'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add dry_run_db if found and the bot in dry run
|
if self.args.db_url != constants.DEFAULT_DB_PROD_URL:
|
||||||
if self.args.dry_run_db and config.get('dry_run', False):
|
config.update({'db_url': self.args.db_url})
|
||||||
config.update({'dry_run_db': True})
|
logger.info('Parameter --db-url detected ...')
|
||||||
logger.info('Parameter --dry-run-db detected ...')
|
|
||||||
|
|
||||||
if config.get('dry_run_db', False):
|
|
||||||
if config.get('dry_run', False):
|
if config.get('dry_run', False):
|
||||||
logger.info('Dry_run will use the DB file: "tradesv3.dry_run.sqlite"')
|
logger.info('Dry run is enabled')
|
||||||
|
if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]:
|
||||||
|
# Default to in-memory db for dry_run if not specified
|
||||||
|
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
|
||||||
else:
|
else:
|
||||||
logger.info('Dry run is disabled. (--dry_run_db ignored)')
|
if not config.get('db_url', None):
|
||||||
|
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
||||||
|
logger.info('Dry run is disabled')
|
||||||
|
|
||||||
|
logger.info('Using DB: "{}"'.format(config['db_url']))
|
||||||
|
|
||||||
# Check if the exchange set by the user is supported
|
# Check if the exchange set by the user is supported
|
||||||
self.check_exchange(config)
|
self.check_exchange(config)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
def _create_default_datadir(self, config: Dict[str, Any]) -> str:
|
||||||
|
exchange_name = config.get('exchange', {}).get('name').lower()
|
||||||
|
default_path = os.path.join('user_data', 'data', exchange_name)
|
||||||
|
if not os.path.isdir(default_path):
|
||||||
|
os.makedirs(default_path)
|
||||||
|
logger.info(f'Created data directory: {default_path}')
|
||||||
|
return default_path
|
||||||
|
|
||||||
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Extract information for sys.argv and load Backtesting configuration
|
Extract information for sys.argv and load Backtesting configuration
|
||||||
@ -145,7 +156,9 @@ class Configuration(object):
|
|||||||
# If --datadir is used we add it to the configuration
|
# If --datadir is used we add it to the configuration
|
||||||
if 'datadir' in self.args and self.args.datadir:
|
if 'datadir' in self.args and self.args.datadir:
|
||||||
config.update({'datadir': self.args.datadir})
|
config.update({'datadir': self.args.datadir})
|
||||||
logger.info('Parameter --datadir detected: %s ...', self.args.datadir)
|
else:
|
||||||
|
config.update({'datadir': self._create_default_datadir(config)})
|
||||||
|
logger.info('Using data folder: %s ...', config.get('datadir'))
|
||||||
|
|
||||||
# If -r/--refresh-pairs-cached is used we add it to the configuration
|
# If -r/--refresh-pairs-cached is used we add it to the configuration
|
||||||
if 'refresh_pairs' in self.args and self.args.refresh_pairs:
|
if 'refresh_pairs' in self.args and self.args.refresh_pairs:
|
||||||
@ -157,6 +170,11 @@ class Configuration(object):
|
|||||||
config.update({'export': self.args.export})
|
config.update({'export': self.args.export})
|
||||||
logger.info('Parameter --export detected: %s ...', self.args.export)
|
logger.info('Parameter --export detected: %s ...', self.args.export)
|
||||||
|
|
||||||
|
# If --export-filename is used we add it to the configuration
|
||||||
|
if 'export' in config and 'exportfilename' in self.args and self.args.exportfilename:
|
||||||
|
config.update({'exportfilename': self.args.exportfilename})
|
||||||
|
logger.info('Storing backtest results to %s ...', self.args.exportfilename)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def _load_hyperopt_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
def _load_hyperopt_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@ -192,7 +210,7 @@ class Configuration(object):
|
|||||||
validate(conf, constants.CONF_SCHEMA)
|
validate(conf, constants.CONF_SCHEMA)
|
||||||
return conf
|
return conf
|
||||||
except ValidationError as exception:
|
except ValidationError as exception:
|
||||||
logger.fatal(
|
logger.critical(
|
||||||
'Invalid configuration. See config.json.example. Reason: %s',
|
'Invalid configuration. See config.json.example. Reason: %s',
|
||||||
exception
|
exception
|
||||||
)
|
)
|
||||||
@ -218,9 +236,8 @@ class Configuration(object):
|
|||||||
exchange = config.get('exchange', {}).get('name').lower()
|
exchange = config.get('exchange', {}).get('name').lower()
|
||||||
if exchange not in ccxt.exchanges:
|
if exchange not in ccxt.exchanges:
|
||||||
|
|
||||||
exception_msg = 'Exchange "{}" not supported.\n' \
|
exception_msg = f'Exchange "{exchange}" not supported.\n' \
|
||||||
'The following exchanges are supported: {}'\
|
f'The following exchanges are supported: {", ".join(ccxt.exchanges)}'
|
||||||
.format(exchange, ', '.join(ccxt.exchanges))
|
|
||||||
|
|
||||||
logger.critical(exception_msg)
|
logger.critical(exception_msg)
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
|
@ -9,9 +9,12 @@ TICKER_INTERVAL = 5 # min
|
|||||||
HYPEROPT_EPOCH = 100 # epochs
|
HYPEROPT_EPOCH = 100 # epochs
|
||||||
RETRY_TIMEOUT = 30 # sec
|
RETRY_TIMEOUT = 30 # sec
|
||||||
DEFAULT_STRATEGY = 'DefaultStrategy'
|
DEFAULT_STRATEGY = 'DefaultStrategy'
|
||||||
|
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
||||||
|
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
|
||||||
|
|
||||||
TICKER_INTERVAL_MINUTES = {
|
TICKER_INTERVAL_MINUTES = {
|
||||||
'1m': 1,
|
'1m': 1,
|
||||||
|
'3m': 3,
|
||||||
'5m': 5,
|
'5m': 5,
|
||||||
'15m': 15,
|
'15m': 15,
|
||||||
'30m': 30,
|
'30m': 30,
|
||||||
@ -19,11 +22,20 @@ TICKER_INTERVAL_MINUTES = {
|
|||||||
'2h': 120,
|
'2h': 120,
|
||||||
'4h': 240,
|
'4h': 240,
|
||||||
'6h': 360,
|
'6h': 360,
|
||||||
|
'8h': 480,
|
||||||
'12h': 720,
|
'12h': 720,
|
||||||
'1d': 1440,
|
'1d': 1440,
|
||||||
|
'3d': 4320,
|
||||||
'1w': 10080,
|
'1w': 10080,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SUPPORTED_FIAT = [
|
||||||
|
"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK",
|
||||||
|
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
|
||||||
|
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
|
||||||
|
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD",
|
||||||
|
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
|
||||||
|
]
|
||||||
|
|
||||||
# Required json-schema for user specified config
|
# Required json-schema for user specified config
|
||||||
CONF_SCHEMA = {
|
CONF_SCHEMA = {
|
||||||
@ -31,16 +43,9 @@ CONF_SCHEMA = {
|
|||||||
'properties': {
|
'properties': {
|
||||||
'max_open_trades': {'type': 'integer', 'minimum': 0},
|
'max_open_trades': {'type': 'integer', 'minimum': 0},
|
||||||
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
|
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
|
||||||
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
|
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT', 'EUR', 'USD']},
|
||||||
'stake_amount': {'type': 'number', 'minimum': 0.0005},
|
'stake_amount': {'type': 'number', 'minimum': 0.0005},
|
||||||
'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF',
|
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
|
||||||
'CLP', 'CNY', 'CZK', 'DKK',
|
|
||||||
'EUR', 'GBP', 'HKD', 'HUF',
|
|
||||||
'IDR', 'ILS', 'INR', 'JPY',
|
|
||||||
'KRW', 'MXN', 'MYR', 'NOK',
|
|
||||||
'NZD', 'PHP', 'PKR', 'PLN',
|
|
||||||
'RUB', 'SEK', 'SGD', 'THB',
|
|
||||||
'TRY', 'TWD', 'ZAR', 'USD']},
|
|
||||||
'dry_run': {'type': 'boolean'},
|
'dry_run': {'type': 'boolean'},
|
||||||
'minimal_roi': {
|
'minimal_roi': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
@ -60,9 +65,19 @@ CONF_SCHEMA = {
|
|||||||
'maximum': 1,
|
'maximum': 1,
|
||||||
'exclusiveMaximum': False
|
'exclusiveMaximum': False
|
||||||
},
|
},
|
||||||
|
'use_book_order': {'type': 'boolean'},
|
||||||
|
'book_order_top': {'type': 'number', 'maximum':20,'minimum':1}
|
||||||
},
|
},
|
||||||
'required': ['ask_last_balance']
|
'required': ['ask_last_balance']
|
||||||
},
|
},
|
||||||
|
'ask_strategy': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'use_book_order': {'type': 'boolean'},
|
||||||
|
'book_order_min': {'type': 'number', 'minimum':1},
|
||||||
|
'book_order_max': {'type': 'number', 'minimum':1}
|
||||||
|
},
|
||||||
|
},
|
||||||
'exchange': {'$ref': '#/definitions/exchange'},
|
'exchange': {'$ref': '#/definitions/exchange'},
|
||||||
'experimental': {
|
'experimental': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
@ -80,6 +95,7 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
'required': ['enabled', 'token', 'chat_id']
|
'required': ['enabled', 'token', 'chat_id']
|
||||||
},
|
},
|
||||||
|
'db_url': {'type': 'string'},
|
||||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||||
'internals': {
|
'internals': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
|
@ -18,6 +18,8 @@ _API: ccxt.Exchange = None
|
|||||||
_CONF: Dict = {}
|
_CONF: Dict = {}
|
||||||
API_RETRY_COUNT = 4
|
API_RETRY_COUNT = 4
|
||||||
|
|
||||||
|
_CACHED_TICKER: Dict[str, Any] = {}
|
||||||
|
|
||||||
# Holds all open sell orders for dry_run
|
# Holds all open sell orders for dry_run
|
||||||
_DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {}
|
_DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {}
|
||||||
|
|
||||||
@ -57,7 +59,7 @@ def init_ccxt(exchange_config: dict) -> ccxt.Exchange:
|
|||||||
name = exchange_config['name']
|
name = exchange_config['name']
|
||||||
|
|
||||||
if name not in ccxt.exchanges:
|
if name not in ccxt.exchanges:
|
||||||
raise OperationalException('Exchange {} is not supported'.format(name))
|
raise OperationalException(f'Exchange {name} is not supported')
|
||||||
try:
|
try:
|
||||||
api = getattr(ccxt, name.lower())({
|
api = getattr(ccxt, name.lower())({
|
||||||
'apiKey': exchange_config.get('key'),
|
'apiKey': exchange_config.get('key'),
|
||||||
@ -67,7 +69,7 @@ def init_ccxt(exchange_config: dict) -> ccxt.Exchange:
|
|||||||
'enableRateLimit': True,
|
'enableRateLimit': True,
|
||||||
})
|
})
|
||||||
except (KeyError, AttributeError):
|
except (KeyError, AttributeError):
|
||||||
raise OperationalException('Exchange {} is not supported'.format(name))
|
raise OperationalException(f'Exchange {name} is not supported')
|
||||||
|
|
||||||
return api
|
return api
|
||||||
|
|
||||||
@ -116,11 +118,10 @@ def validate_pairs(pairs: List[str]) -> None:
|
|||||||
# TODO: add a support for having coins in BTC/USDT format
|
# TODO: add a support for having coins in BTC/USDT format
|
||||||
if not pair.endswith(stake_cur):
|
if not pair.endswith(stake_cur):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur)
|
f'Pair {pair} not compatible with stake_currency: {stake_cur}')
|
||||||
)
|
|
||||||
if pair not in markets:
|
if pair not in markets:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'Pair {} is not available at {}'.format(pair, get_name()))
|
f'Pair {pair} is not available at {get_name()}')
|
||||||
|
|
||||||
|
|
||||||
def exchange_has(endpoint: str) -> bool:
|
def exchange_has(endpoint: str) -> bool:
|
||||||
@ -136,7 +137,7 @@ def exchange_has(endpoint: str) -> bool:
|
|||||||
def buy(pair: str, rate: float, amount: float) -> Dict:
|
def buy(pair: str, rate: float, amount: float) -> Dict:
|
||||||
if _CONF['dry_run']:
|
if _CONF['dry_run']:
|
||||||
global _DRY_RUN_OPEN_ORDERS
|
global _DRY_RUN_OPEN_ORDERS
|
||||||
order_id = 'dry_run_buy_{}'.format(randint(0, 10**6))
|
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
||||||
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
||||||
'pair': pair,
|
'pair': pair,
|
||||||
'price': rate,
|
'price': rate,
|
||||||
@ -154,20 +155,17 @@ def buy(pair: str, rate: float, amount: float) -> Dict:
|
|||||||
return _API.create_limit_buy_order(pair, amount, rate)
|
return _API.create_limit_buy_order(pair, amount, rate)
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
'Insufficient funds to create limit buy order on market {}.'
|
f'Insufficient funds to create limit buy order on market {pair}.'
|
||||||
'Tried to buy amount {} at rate {} (total {}).'
|
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
|
||||||
'Message: {}'.format(pair, amount, rate, rate*amount, e)
|
f'Message: {e}')
|
||||||
)
|
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
'Could not create limit buy order on market {}.'
|
f'Could not create limit buy order on market {pair}.'
|
||||||
'Tried to buy amount {} at rate {} (total {}).'
|
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
|
||||||
'Message: {}'.format(pair, amount, rate, rate*amount, e)
|
f'Message: {e}')
|
||||||
)
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not place buy order due to {}. Message: {}'.format(
|
f'Could not place buy order due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
@ -175,7 +173,7 @@ def buy(pair: str, rate: float, amount: float) -> Dict:
|
|||||||
def sell(pair: str, rate: float, amount: float) -> Dict:
|
def sell(pair: str, rate: float, amount: float) -> Dict:
|
||||||
if _CONF['dry_run']:
|
if _CONF['dry_run']:
|
||||||
global _DRY_RUN_OPEN_ORDERS
|
global _DRY_RUN_OPEN_ORDERS
|
||||||
order_id = 'dry_run_sell_{}'.format(randint(0, 10**6))
|
order_id = f'dry_run_sell_{randint(0, 10**6)}'
|
||||||
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
||||||
'pair': pair,
|
'pair': pair,
|
||||||
'price': rate,
|
'price': rate,
|
||||||
@ -192,20 +190,17 @@ def sell(pair: str, rate: float, amount: float) -> Dict:
|
|||||||
return _API.create_limit_sell_order(pair, amount, rate)
|
return _API.create_limit_sell_order(pair, amount, rate)
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
'Insufficient funds to create limit sell order on market {}.'
|
f'Insufficient funds to create limit sell order on market {pair}.'
|
||||||
'Tried to sell amount {} at rate {} (total {}).'
|
f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).'
|
||||||
'Message: {}'.format(pair, amount, rate, rate*amount, e)
|
f'Message: {e}')
|
||||||
)
|
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
'Could not create limit sell order on market {}.'
|
f'Could not create limit sell order on market {pair}.'
|
||||||
'Tried to sell amount {} at rate {} (total {}).'
|
f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).'
|
||||||
'Message: {}'.format(pair, amount, rate, rate*amount, e)
|
f'Message: {e}')
|
||||||
)
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not place sell order due to {}. Message: {}'.format(
|
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
@ -220,8 +215,7 @@ def get_balance(currency: str) -> float:
|
|||||||
balance = balances.get(currency)
|
balance = balances.get(currency)
|
||||||
if balance is None:
|
if balance is None:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not get {} balance due to malformed exchange response: {}'.format(
|
f'Could not get {currency} balance due to malformed exchange response: {balances}')
|
||||||
currency, balances))
|
|
||||||
return balance['free']
|
return balance['free']
|
||||||
|
|
||||||
|
|
||||||
@ -241,11 +235,23 @@ def get_balances() -> dict:
|
|||||||
return balances
|
return balances
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not get balance due to {}. Message: {}'.format(
|
f'Could not get balance due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def get_order_book(pair: str, refresh: Optional[bool] = True) -> dict:
|
||||||
|
try:
|
||||||
|
return _API.fetch_order_book(pair)
|
||||||
|
except ccxt.NotSupported as e:
|
||||||
|
raise OperationalException(
|
||||||
|
f'Exchange {_API.name} does not support fetching order book.'
|
||||||
|
f'Message: {e}')
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not load order book due to {e.__class__.__name__}. Message: {e}')
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e)
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_tickers() -> Dict:
|
def get_tickers() -> Dict:
|
||||||
@ -253,28 +259,37 @@ def get_tickers() -> Dict:
|
|||||||
return _API.fetch_tickers()
|
return _API.fetch_tickers()
|
||||||
except ccxt.NotSupported as e:
|
except ccxt.NotSupported as e:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'Exchange {} does not support fetching tickers in batch.'
|
f'Exchange {_API.name} does not support fetching tickers in batch.'
|
||||||
'Message: {}'.format(_API.name, e)
|
f'Message: {e}')
|
||||||
)
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not load tickers due to {}. Message: {}'.format(
|
f'Could not load tickers due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
|
|
||||||
# TODO: remove refresh argument, keeping it to keep track of where it was intended to be used
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict:
|
def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict:
|
||||||
|
global _CACHED_TICKER
|
||||||
|
if refresh or pair not in _CACHED_TICKER.keys():
|
||||||
try:
|
try:
|
||||||
return _API.fetch_ticker(pair)
|
data = _API.fetch_ticker(pair)
|
||||||
|
try:
|
||||||
|
_CACHED_TICKER[pair] = {
|
||||||
|
'bid': float(data['bid']),
|
||||||
|
'ask': float(data['ask']),
|
||||||
|
}
|
||||||
|
except KeyError:
|
||||||
|
logger.debug("Could not cache ticker data for %s", pair)
|
||||||
|
return data
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not load ticker history due to {}. Message: {}'.format(
|
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
else:
|
||||||
|
logger.info("returning cached ticker-data for %s", pair)
|
||||||
|
return _CACHED_TICKER[pair]
|
||||||
|
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
@ -290,10 +305,15 @@ def get_ticker_history(pair: str, tick_interval: str, since_ms: Optional[int] =
|
|||||||
# chached data was already downloaded
|
# chached data was already downloaded
|
||||||
till_time_ms = min(till_time_ms, arrow.utcnow().shift(minutes=-10).timestamp * 1000)
|
till_time_ms = min(till_time_ms, arrow.utcnow().shift(minutes=-10).timestamp * 1000)
|
||||||
|
|
||||||
data = []
|
data: List[Dict[Any, Any]] = []
|
||||||
while not since_ms or since_ms < till_time_ms:
|
while not since_ms or since_ms < till_time_ms:
|
||||||
data_part = _API.fetch_ohlcv(pair, timeframe=tick_interval, since=since_ms)
|
data_part = _API.fetch_ohlcv(pair, timeframe=tick_interval, since=since_ms)
|
||||||
|
|
||||||
|
# Because some exchange sort Tickers ASC and other DESC.
|
||||||
|
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last)
|
||||||
|
# when GDAX returns a list of tickers DESC (newest first, oldest last)
|
||||||
|
data_part = sorted(data_part, key=lambda x: x[0])
|
||||||
|
|
||||||
if not data_part:
|
if not data_part:
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -308,15 +328,13 @@ def get_ticker_history(pair: str, tick_interval: str, since_ms: Optional[int] =
|
|||||||
return data
|
return data
|
||||||
except ccxt.NotSupported as e:
|
except ccxt.NotSupported as e:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'Exchange {} does not support fetching historical candlestick data.'
|
f'Exchange {_API.name} does not support fetching historical candlestick data.'
|
||||||
'Message: {}'.format(_API.name, e)
|
f'Message: {e}')
|
||||||
)
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not load ticker history due to {}. Message: {}'.format(
|
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException('Could not fetch ticker data. Msg: {}'.format(e))
|
raise OperationalException(f'Could not fetch ticker data. Msg: {e}')
|
||||||
|
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
@ -328,12 +346,10 @@ def cancel_order(order_id: str, pair: str) -> None:
|
|||||||
return _API.cancel_order(order_id, pair)
|
return _API.cancel_order(order_id, pair)
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
'Could not cancel order. Message: {}'.format(e)
|
f'Could not cancel order. Message: {e}')
|
||||||
)
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not cancel order due to {}. Message: {}'.format(
|
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
@ -350,12 +366,10 @@ def get_order(order_id: str, pair: str) -> Dict:
|
|||||||
return _API.fetch_order(order_id, pair)
|
return _API.fetch_order(order_id, pair)
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
'Could not get order. Message: {}'.format(e)
|
f'Could not get order. Message: {e}')
|
||||||
)
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not get order due to {}. Message: {}'.format(
|
f'Could not get order due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
@ -374,8 +388,7 @@ def get_trades_for_order(order_id: str, pair: str, since: datetime) -> List:
|
|||||||
|
|
||||||
except ccxt.NetworkError as e:
|
except ccxt.NetworkError as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not get trades due to networking error. Message: {}'.format(e)
|
f'Could not get trades due to networking error. Message: {e}')
|
||||||
)
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
@ -397,8 +410,7 @@ def get_markets() -> List[dict]:
|
|||||||
return _API.fetch_markets()
|
return _API.fetch_markets()
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not load markets due to {}. Message: {}'.format(
|
f'Could not load markets due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
@ -423,8 +435,7 @@ def get_fee(symbol='ETH/BTC', type='', side='', amount=1,
|
|||||||
price=price, takerOrMaker=taker_or_maker)['rate']
|
price=price, takerOrMaker=taker_or_maker)['rate']
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not get fee info due to {}. Message: {}'.format(
|
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
|
@ -5,9 +5,11 @@ e.g BTC to USD
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import Dict
|
from typing import Dict, List
|
||||||
|
|
||||||
from coinmarketcap import Market
|
from coinmarketcap import Market
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
from freqtrade.constants import SUPPORTED_FIAT
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -33,7 +35,7 @@ class CryptoFiat(object):
|
|||||||
self.price = 0.0
|
self.price = 0.0
|
||||||
|
|
||||||
# Private attributes
|
# Private attributes
|
||||||
self._expiration = 0
|
self._expiration = 0.0
|
||||||
|
|
||||||
self.crypto_symbol = crypto_symbol.upper()
|
self.crypto_symbol = crypto_symbol.upper()
|
||||||
self.fiat_symbol = fiat_symbol.upper()
|
self.fiat_symbol = fiat_symbol.upper()
|
||||||
@ -64,15 +66,7 @@ class CryptoToFiatConverter(object):
|
|||||||
This object is also a Singleton
|
This object is also a Singleton
|
||||||
"""
|
"""
|
||||||
__instance = None
|
__instance = None
|
||||||
_coinmarketcap = None
|
_coinmarketcap: Market = None
|
||||||
|
|
||||||
# Constants
|
|
||||||
SUPPORTED_FIAT = [
|
|
||||||
"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK",
|
|
||||||
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
|
|
||||||
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
|
|
||||||
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD"
|
|
||||||
]
|
|
||||||
|
|
||||||
_cryptomap: Dict = {}
|
_cryptomap: Dict = {}
|
||||||
|
|
||||||
@ -86,7 +80,7 @@ class CryptoToFiatConverter(object):
|
|||||||
return CryptoToFiatConverter.__instance
|
return CryptoToFiatConverter.__instance
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._pairs = []
|
self._pairs: List[CryptoFiat] = []
|
||||||
self._load_cryptomap()
|
self._load_cryptomap()
|
||||||
|
|
||||||
def _load_cryptomap(self) -> None:
|
def _load_cryptomap(self) -> None:
|
||||||
@ -94,8 +88,11 @@ class CryptoToFiatConverter(object):
|
|||||||
coinlistings = self._coinmarketcap.listings()
|
coinlistings = self._coinmarketcap.listings()
|
||||||
self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])),
|
self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])),
|
||||||
coinlistings["data"]))
|
coinlistings["data"]))
|
||||||
except ValueError:
|
except (ValueError, RequestException) as exception:
|
||||||
logger.error("Could not load FIAT Cryptocurrency map")
|
logger.error(
|
||||||
|
"Could not load FIAT Cryptocurrency map for the following problem: %s",
|
||||||
|
exception
|
||||||
|
)
|
||||||
|
|
||||||
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
|
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
|
||||||
"""
|
"""
|
||||||
@ -122,7 +119,7 @@ class CryptoToFiatConverter(object):
|
|||||||
|
|
||||||
# Check if the fiat convertion you want is supported
|
# Check if the fiat convertion you want is supported
|
||||||
if not self._is_supported_fiat(fiat=fiat_symbol):
|
if not self._is_supported_fiat(fiat=fiat_symbol):
|
||||||
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
|
raise ValueError(f'The fiat {fiat_symbol} is not supported.')
|
||||||
|
|
||||||
# Get the pair that interest us and return the price in fiat
|
# Get the pair that interest us and return the price in fiat
|
||||||
for pair in self._pairs:
|
for pair in self._pairs:
|
||||||
@ -174,7 +171,7 @@ class CryptoToFiatConverter(object):
|
|||||||
|
|
||||||
fiat = fiat.upper()
|
fiat = fiat.upper()
|
||||||
|
|
||||||
return fiat in self.SUPPORTED_FIAT
|
return fiat in SUPPORTED_FIAT
|
||||||
|
|
||||||
def _find_price(self, crypto_symbol: str, fiat_symbol: str) -> float:
|
def _find_price(self, crypto_symbol: str, fiat_symbol: str) -> float:
|
||||||
"""
|
"""
|
||||||
@ -185,12 +182,17 @@ class CryptoToFiatConverter(object):
|
|||||||
"""
|
"""
|
||||||
# Check if the fiat convertion you want is supported
|
# Check if the fiat convertion you want is supported
|
||||||
if not self._is_supported_fiat(fiat=fiat_symbol):
|
if not self._is_supported_fiat(fiat=fiat_symbol):
|
||||||
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
|
raise ValueError(f'The fiat {fiat_symbol} is not supported.')
|
||||||
|
|
||||||
|
# No need to convert if both crypto and fiat are the same
|
||||||
|
if crypto_symbol == fiat_symbol:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
if crypto_symbol not in self._cryptomap:
|
if crypto_symbol not in self._cryptomap:
|
||||||
# return 0 for unsupported stake currencies (fiat-convert should not break the bot)
|
# return 0 for unsupported stake currencies (fiat-convert should not break the bot)
|
||||||
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
|
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return float(
|
return float(
|
||||||
self._coinmarketcap.ticker(
|
self._coinmarketcap.ticker(
|
||||||
@ -198,6 +200,6 @@ class CryptoToFiatConverter(object):
|
|||||||
convert=fiat_symbol
|
convert=fiat_symbol
|
||||||
)['data']['quotes'][fiat_symbol.upper()]['price']
|
)['data']['quotes'][fiat_symbol.upper()]['price']
|
||||||
)
|
)
|
||||||
except BaseException as ex:
|
except BaseException as exception:
|
||||||
logger.error("Error in _find_price: %s", ex)
|
logger.error("Error in _find_price: %s", exception)
|
||||||
return 0.0
|
return 0.0
|
||||||
|
@ -33,12 +33,11 @@ class FreqtradeBot(object):
|
|||||||
This is from here the bot start its logic.
|
This is from here the bot start its logic.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config: Dict[str, Any], db_url: Optional[str] = None):
|
def __init__(self, config: Dict[str, Any])-> None:
|
||||||
"""
|
"""
|
||||||
Init all variables and object the bot need to work
|
Init all variables and object the bot need to work
|
||||||
:param config: configuration dict, you can use the Configuration.get_config()
|
:param config: configuration dict, you can use the Configuration.get_config()
|
||||||
method to get the config dict.
|
method to get the config dict.
|
||||||
:param db_url: database connector string for sqlalchemy (Optional)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -51,26 +50,22 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
# Init objects
|
# Init objects
|
||||||
self.config = config
|
self.config = config
|
||||||
self.analyze = None
|
self.analyze = Analyze(self.config)
|
||||||
self.fiat_converter = None
|
self.fiat_converter = CryptoToFiatConverter()
|
||||||
self.rpc = None
|
self.rpc: RPCManager = RPCManager(self)
|
||||||
self.persistence = None
|
self.persistence = None
|
||||||
self.exchange = None
|
self.exchange = None
|
||||||
|
|
||||||
self._init_modules(db_url=db_url)
|
self._init_modules()
|
||||||
|
|
||||||
def _init_modules(self, db_url: Optional[str] = None) -> None:
|
def _init_modules(self) -> None:
|
||||||
"""
|
"""
|
||||||
Initializes all modules and updates the config
|
Initializes all modules and updates the config
|
||||||
:param db_url: database connector string for sqlalchemy (Optional)
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# Initialize all modules
|
# Initialize all modules
|
||||||
self.analyze = Analyze(self.config)
|
|
||||||
self.fiat_converter = CryptoToFiatConverter()
|
|
||||||
self.rpc = RPCManager(self)
|
|
||||||
|
|
||||||
persistence.init(self.config, db_url)
|
persistence.init(self.config)
|
||||||
exchange.init(self.config)
|
exchange.init(self.config)
|
||||||
|
|
||||||
# Set initial application state
|
# Set initial application state
|
||||||
@ -93,7 +88,7 @@ class FreqtradeBot(object):
|
|||||||
persistence.cleanup()
|
persistence.cleanup()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def worker(self, old_state: None) -> State:
|
def worker(self, old_state: State = None) -> State:
|
||||||
"""
|
"""
|
||||||
Trading routine that must be run at each loop
|
Trading routine that must be run at each loop
|
||||||
:param old_state: the previous service state from the previous call
|
:param old_state: the previous service state from the previous call
|
||||||
@ -102,7 +97,7 @@ class FreqtradeBot(object):
|
|||||||
# Log state transition
|
# Log state transition
|
||||||
state = self.state
|
state = self.state
|
||||||
if state != old_state:
|
if state != old_state:
|
||||||
self.rpc.send_msg('*Status:* `{}`'.format(state.name.lower()))
|
self.rpc.send_msg(f'*Status:* `{state.name.lower()}`')
|
||||||
logger.info('Changing state to: %s', state.name)
|
logger.info('Changing state to: %s', state.name)
|
||||||
|
|
||||||
if state == State.STOPPED:
|
if state == State.STOPPED:
|
||||||
@ -176,12 +171,10 @@ class FreqtradeBot(object):
|
|||||||
logger.warning('%s, retrying in 30 seconds...', error)
|
logger.warning('%s, retrying in 30 seconds...', error)
|
||||||
time.sleep(constants.RETRY_TIMEOUT)
|
time.sleep(constants.RETRY_TIMEOUT)
|
||||||
except OperationalException:
|
except OperationalException:
|
||||||
|
tb = traceback.format_exc()
|
||||||
|
hint = 'Issue `/start` if you think it is safe to restart.'
|
||||||
self.rpc.send_msg(
|
self.rpc.send_msg(
|
||||||
'*Status:* OperationalException:\n```\n{traceback}```{hint}'
|
f'*Status:* OperationalException:\n```\n{tb}```{hint}'
|
||||||
.format(
|
|
||||||
traceback=traceback.format_exc(),
|
|
||||||
hint='Issue `/start` if you think it is safe to restart.'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
logger.exception('OperationalException. Stopping trader ...')
|
logger.exception('OperationalException. Stopping trader ...')
|
||||||
self.state = State.STOPPED
|
self.state = State.STOPPED
|
||||||
@ -244,27 +237,37 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
return final_list
|
return final_list
|
||||||
|
|
||||||
def get_target_bid(self, ticker: Dict[str, float]) -> float:
|
def get_target_bid(self, pair: str) -> float:
|
||||||
"""
|
"""
|
||||||
Calculates bid target between current ask price and last price
|
Calculates bid target between current ask price and last price
|
||||||
:param ticker: Ticker to use for getting Ask and Last Price
|
:param ticker: Ticker to use for getting Ask and Last Price
|
||||||
:return: float: Price
|
:return: float: Price
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if self.config['bid_strategy']['use_book_order']:
|
||||||
|
logger.info('Getting price from Order Book')
|
||||||
|
orderBook = exchange.get_order_book(pair)
|
||||||
|
return orderBook['bids'][self.config['bid_strategy']['book_order_top']][0]
|
||||||
|
else:
|
||||||
|
logger.info('Using Ask / Last Price')
|
||||||
|
ticker = exchange.get_ticker(pair);
|
||||||
if ticker['ask'] < ticker['last']:
|
if ticker['ask'] < ticker['last']:
|
||||||
return ticker['ask']
|
return ticker['ask']
|
||||||
balance = self.config['bid_strategy']['ask_last_balance']
|
balance = self.config['bid_strategy']['ask_last_balance']
|
||||||
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
||||||
|
|
||||||
|
|
||||||
def create_trade(self) -> bool:
|
def create_trade(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks the implemented trading indicator(s) for a randomly picked pair,
|
Checks the implemented trading indicator(s) for a randomly picked pair,
|
||||||
if one pair triggers the buy_signal a new trade record gets created
|
if one pair triggers the buy_signal a new trade record gets created
|
||||||
:param stake_amount: amount of btc to spend
|
|
||||||
:param interval: Ticker interval used for Analyze
|
|
||||||
:return: True if a trade object has been created and persisted, False otherwise
|
:return: True if a trade object has been created and persisted, False otherwise
|
||||||
"""
|
"""
|
||||||
stake_amount = self.config['stake_amount']
|
stake_amount = self.config['stake_amount']
|
||||||
interval = self.analyze.get_ticker_interval()
|
interval = self.analyze.get_ticker_interval()
|
||||||
|
stake_currency = self.config['stake_currency']
|
||||||
|
fiat_currency = self.config['fiat_display_currency']
|
||||||
|
exc_name = exchange.get_name()
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'Checking buy signals to create a new trade with stake_amount: %f ...',
|
'Checking buy signals to create a new trade with stake_amount: %f ...',
|
||||||
@ -272,10 +275,9 @@ class FreqtradeBot(object):
|
|||||||
)
|
)
|
||||||
whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])
|
whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])
|
||||||
# Check if stake_amount is fulfilled
|
# Check if stake_amount is fulfilled
|
||||||
if exchange.get_balance(self.config['stake_currency']) < stake_amount:
|
if exchange.get_balance(stake_currency) < stake_amount:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
'stake amount is not fulfilled (currency={})'.format(self.config['stake_currency'])
|
f'stake amount is not fulfilled (currency={stake_currency})')
|
||||||
)
|
|
||||||
|
|
||||||
# Remove currently opened and latest pairs from whitelist
|
# Remove currently opened and latest pairs from whitelist
|
||||||
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
||||||
@ -294,32 +296,25 @@ class FreqtradeBot(object):
|
|||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
pair_s = pair.replace('_', '/')
|
||||||
|
pair_url = exchange.get_pair_detail_url(pair)
|
||||||
# Calculate amount
|
# Calculate amount
|
||||||
buy_limit = self.get_target_bid(exchange.get_ticker(pair))
|
buy_limit = self.get_target_bid(pair)
|
||||||
amount = stake_amount / buy_limit
|
amount = stake_amount / buy_limit
|
||||||
|
|
||||||
order_id = exchange.buy(pair, buy_limit, amount)['id']
|
order_id = exchange.buy(pair, buy_limit, amount)['id']
|
||||||
|
|
||||||
stake_amount_fiat = self.fiat_converter.convert_amount(
|
stake_amount_fiat = self.fiat_converter.convert_amount(
|
||||||
stake_amount,
|
stake_amount,
|
||||||
self.config['stake_currency'],
|
stake_currency,
|
||||||
self.config['fiat_display_currency']
|
fiat_currency
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create trade entity and return
|
# Create trade entity and return
|
||||||
self.rpc.send_msg(
|
self.rpc.send_msg(
|
||||||
'*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '
|
f"""*{exc_name}:* Buying [{pair_s}]({pair_url}) \
|
||||||
.format(
|
with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
||||||
exchange.get_name(),
|
{stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`"""
|
||||||
pair.replace('_', '/'),
|
|
||||||
exchange.get_pair_detail_url(pair),
|
|
||||||
buy_limit,
|
|
||||||
stake_amount,
|
|
||||||
self.config['stake_currency'],
|
|
||||||
stake_amount_fiat,
|
|
||||||
self.config['fiat_display_currency']
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
fee = exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
fee = exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||||
@ -420,12 +415,12 @@ class FreqtradeBot(object):
|
|||||||
fee_abs += exectrade['fee']['cost']
|
fee_abs += exectrade['fee']['cost']
|
||||||
|
|
||||||
if amount != order_amount:
|
if amount != order_amount:
|
||||||
logger.warning("amount {} does not match amount {}".format(amount, trade.amount))
|
logger.warning(f"amount {amount} does not match amount {trade.amount}")
|
||||||
raise OperationalException("Half bought? Amounts don't match")
|
raise OperationalException("Half bought? Amounts don't match")
|
||||||
real_amount = amount - fee_abs
|
real_amount = amount - fee_abs
|
||||||
if fee_abs != 0:
|
if fee_abs != 0:
|
||||||
logger.info("Applying fee on amount for {} (from {} to {}) from Trades".format(
|
logger.info(f"""Applying fee on amount for {trade} \
|
||||||
trade, order['amount'], real_amount))
|
(from {order_amount} to {real_amount}) from Trades""")
|
||||||
return real_amount
|
return real_amount
|
||||||
|
|
||||||
def handle_trade(self, trade: Trade) -> bool:
|
def handle_trade(self, trade: Trade) -> bool:
|
||||||
@ -434,22 +429,39 @@ class FreqtradeBot(object):
|
|||||||
:return: True if trade has been sold, False otherwise
|
:return: True if trade has been sold, False otherwise
|
||||||
"""
|
"""
|
||||||
if not trade.is_open:
|
if not trade.is_open:
|
||||||
raise ValueError('attempt to handle closed trade: {}'.format(trade))
|
raise ValueError(f'attempt to handle closed trade: {trade}')
|
||||||
|
|
||||||
logger.debug('Handling %s ...', trade)
|
logger.debug('Handling %s ...', trade)
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
sell_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
|
|
||||||
(buy, sell) = (False, False)
|
(buy, sell) = (False, False)
|
||||||
|
|
||||||
if self.config.get('experimental', {}).get('use_sell_signal'):
|
if self.config.get('experimental', {}).get('use_sell_signal'):
|
||||||
(buy, sell) = self.analyze.get_signal(trade.pair, self.analyze.get_ticker_interval())
|
(buy, sell) = self.analyze.get_signal(trade.pair, self.analyze.get_ticker_interval())
|
||||||
|
|
||||||
if self.analyze.should_sell(trade, current_rate, datetime.utcnow(), buy, sell):
|
if self.config['ask_strategy']['use_book_order']:
|
||||||
self.execute_sell(trade, current_rate)
|
logger.info('Using order book for selling...')
|
||||||
|
orderBook = exchange.get_order_book(trade.pair)
|
||||||
|
# logger.debug('Order book %s',orderBook)
|
||||||
|
for i in range(self.config['ask_strategy']['book_order_min'],self.config['ask_strategy']['book_order_max']):
|
||||||
|
sell_rate = orderBook['asks'][i-1][0]
|
||||||
|
logger.debug('checking sell rate %s) %.8f > %.8f (%f %%)',i,trade.open_rate,sell_rate,((sell_rate/trade.open_rate)*100)-100)
|
||||||
|
if self.check_sell(trade, sell_rate, buy, sell):
|
||||||
return True
|
return True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if self.check_sell(trade, sell_rate, buy, sell):
|
||||||
|
return True
|
||||||
|
|
||||||
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
|
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool ) -> bool:
|
||||||
|
if self.analyze.should_sell(trade, sell_rate, datetime.utcnow(), buy, sell):
|
||||||
|
self.execute_sell(trade, sell_rate)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def check_handle_timedout(self, timeoutvalue: int) -> None:
|
def check_handle_timedout(self, timeoutvalue: int) -> None:
|
||||||
"""
|
"""
|
||||||
Check if any orders are timed out and cancel if neccessary
|
Check if any orders are timed out and cancel if neccessary
|
||||||
@ -460,6 +472,12 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
||||||
try:
|
try:
|
||||||
|
# FIXME: Somehow the query above returns results
|
||||||
|
# where the open_order_id is in fact None.
|
||||||
|
# This is probably because the record got
|
||||||
|
# updated via /forcesell in a different thread.
|
||||||
|
if not trade.open_order_id:
|
||||||
|
continue
|
||||||
order = exchange.get_order(trade.open_order_id, trade.pair)
|
order = exchange.get_order(trade.open_order_id, trade.pair)
|
||||||
except requests.exceptions.RequestException:
|
except requests.exceptions.RequestException:
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -485,16 +503,14 @@ class FreqtradeBot(object):
|
|||||||
"""Buy timeout - cancel order
|
"""Buy timeout - cancel order
|
||||||
:return: True if order was fully cancelled
|
:return: True if order was fully cancelled
|
||||||
"""
|
"""
|
||||||
|
pair_s = trade.pair.replace('_', '/')
|
||||||
exchange.cancel_order(trade.open_order_id, trade.pair)
|
exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||||
if order['remaining'] == order['amount']:
|
if order['remaining'] == order['amount']:
|
||||||
# if trade is not partially completed, just delete the trade
|
# if trade is not partially completed, just delete the trade
|
||||||
Trade.session.delete(trade)
|
Trade.session.delete(trade)
|
||||||
# FIX? do we really need to flush, caller of
|
|
||||||
# check_handle_timedout will flush afterwards
|
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
logger.info('Buy order timeout for %s.', trade)
|
logger.info('Buy order timeout for %s.', trade)
|
||||||
self.rpc.send_msg('*Timeout:* Unfilled buy order for {} cancelled'.format(
|
self.rpc.send_msg(f'*Timeout:* Unfilled buy order for {pair_s} cancelled')
|
||||||
trade.pair.replace('_', '/')))
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# if trade is partially complete, edit the stake details for the trade
|
# if trade is partially complete, edit the stake details for the trade
|
||||||
@ -503,8 +519,7 @@ class FreqtradeBot(object):
|
|||||||
trade.stake_amount = trade.amount * trade.open_rate
|
trade.stake_amount = trade.amount * trade.open_rate
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
logger.info('Partial buy order timeout for %s.', trade)
|
logger.info('Partial buy order timeout for %s.', trade)
|
||||||
self.rpc.send_msg('*Timeout:* Remaining buy order for {} cancelled'.format(
|
self.rpc.send_msg(f'*Timeout:* Remaining buy order for {pair_s} cancelled')
|
||||||
trade.pair.replace('_', '/')))
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
|
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
|
||||||
@ -513,6 +528,7 @@ class FreqtradeBot(object):
|
|||||||
Sell timeout - cancel order and update trade
|
Sell timeout - cancel order and update trade
|
||||||
:return: True if order was fully cancelled
|
:return: True if order was fully cancelled
|
||||||
"""
|
"""
|
||||||
|
pair_s = trade.pair.replace('_', '/')
|
||||||
if order['remaining'] == order['amount']:
|
if order['remaining'] == order['amount']:
|
||||||
# if trade is not partially completed, just cancel the trade
|
# if trade is not partially completed, just cancel the trade
|
||||||
exchange.cancel_order(trade.open_order_id, trade.pair)
|
exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||||
@ -521,8 +537,7 @@ class FreqtradeBot(object):
|
|||||||
trade.close_date = None
|
trade.close_date = None
|
||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
self.rpc.send_msg('*Timeout:* Unfilled sell order for {} cancelled'.format(
|
self.rpc.send_msg(f'*Timeout:* Unfilled sell order for {pair_s} cancelled')
|
||||||
trade.pair.replace('_', '/')))
|
|
||||||
logger.info('Sell order timeout for %s.', trade)
|
logger.info('Sell order timeout for %s.', trade)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -536,6 +551,8 @@ class FreqtradeBot(object):
|
|||||||
:param limit: limit rate for the sell order
|
:param limit: limit rate for the sell order
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
exc = trade.exchange
|
||||||
|
pair = trade.pair
|
||||||
# Execute sell and update trade record
|
# Execute sell and update trade record
|
||||||
order_id = exchange.sell(str(trade.pair), limit, trade.amount)['id']
|
order_id = exchange.sell(str(trade.pair), limit, trade.amount)['id']
|
||||||
trade.open_order_id = order_id
|
trade.open_order_id = order_id
|
||||||
@ -545,43 +562,31 @@ class FreqtradeBot(object):
|
|||||||
profit_trade = trade.calc_profit(rate=limit)
|
profit_trade = trade.calc_profit(rate=limit)
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
profit = trade.calc_profit_percent(limit)
|
profit = trade.calc_profit_percent(limit)
|
||||||
|
pair_url = exchange.get_pair_detail_url(trade.pair)
|
||||||
|
gain = "profit" if fmt_exp_profit > 0 else "loss"
|
||||||
|
|
||||||
message = "*{exchange}:* Selling\n" \
|
message = f"*{exc}:* Selling\n" \
|
||||||
"*Current Pair:* [{pair}]({pair_url})\n" \
|
f"*Current Pair:* [{pair}]({pair_url})\n" \
|
||||||
"*Limit:* `{limit}`\n" \
|
f"*Limit:* `{limit}`\n" \
|
||||||
"*Amount:* `{amount}`\n" \
|
f"*Amount:* `{round(trade.amount, 8)}`\n" \
|
||||||
"*Open Rate:* `{open_rate:.8f}`\n" \
|
f"*Open Rate:* `{trade.open_rate:.8f}`\n" \
|
||||||
"*Current Rate:* `{current_rate:.8f}`\n" \
|
f"*Current Rate:* `{current_rate:.8f}`\n" \
|
||||||
"*Profit:* `{profit:.2f}%`" \
|
f"*Profit:* `{round(profit * 100, 2):.2f}%`" \
|
||||||
"".format(
|
""
|
||||||
exchange=trade.exchange,
|
|
||||||
pair=trade.pair,
|
|
||||||
pair_url=exchange.get_pair_detail_url(trade.pair),
|
|
||||||
limit=limit,
|
|
||||||
open_rate=trade.open_rate,
|
|
||||||
current_rate=current_rate,
|
|
||||||
amount=round(trade.amount, 8),
|
|
||||||
profit=round(profit * 100, 2),
|
|
||||||
)
|
|
||||||
|
|
||||||
# For regular case, when the configuration exists
|
# For regular case, when the configuration exists
|
||||||
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
|
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
|
||||||
|
stake = self.config['stake_currency']
|
||||||
|
fiat = self.config['fiat_display_currency']
|
||||||
fiat_converter = CryptoToFiatConverter()
|
fiat_converter = CryptoToFiatConverter()
|
||||||
profit_fiat = fiat_converter.convert_amount(
|
profit_fiat = fiat_converter.convert_amount(
|
||||||
profit_trade,
|
profit_trade,
|
||||||
self.config['stake_currency'],
|
stake,
|
||||||
self.config['fiat_display_currency']
|
fiat
|
||||||
)
|
|
||||||
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f} {coin}`' \
|
|
||||||
'` / {profit_fiat:.3f} {fiat})`' \
|
|
||||||
''.format(
|
|
||||||
gain="profit" if fmt_exp_profit > 0 else "loss",
|
|
||||||
profit_percent=fmt_exp_profit,
|
|
||||||
profit_coin=profit_trade,
|
|
||||||
coin=self.config['stake_currency'],
|
|
||||||
profit_fiat=profit_fiat,
|
|
||||||
fiat=self.config['fiat_display_currency'],
|
|
||||||
)
|
)
|
||||||
|
message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \
|
||||||
|
f'` / {profit_fiat:.3f} {fiat})`'\
|
||||||
|
''
|
||||||
# Because telegram._forcesell does not have the configuration
|
# Because telegram._forcesell does not have the configuration
|
||||||
# Ignore the FIAT value and does not show the stake_currency as well
|
# Ignore the FIAT value and does not show the stake_currency as well
|
||||||
else:
|
else:
|
||||||
|
@ -13,7 +13,7 @@ def went_down(series: Series) -> bool:
|
|||||||
return series < series.shift(1)
|
return series < series.shift(1)
|
||||||
|
|
||||||
|
|
||||||
def ehlers_super_smoother(series: Series, smoothing: float = 6) -> type(Series):
|
def ehlers_super_smoother(series: Series, smoothing: float = 6) -> Series:
|
||||||
magic = pi * sqrt(2) / smoothing
|
magic = pi * sqrt(2) / smoothing
|
||||||
a1 = exp(-magic)
|
a1 = exp(-magic)
|
||||||
coeff2 = 2 * a1 * cos(magic)
|
coeff2 = 2 * a1 * cos(magic)
|
||||||
|
@ -7,6 +7,7 @@ import logging
|
|||||||
import sys
|
import sys
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
from freqtrade import OperationalException
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
@ -47,6 +48,9 @@ def main(sysargv: List[str]) -> None:
|
|||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info('SIGINT received, aborting ...')
|
logger.info('SIGINT received, aborting ...')
|
||||||
return_code = 0
|
return_code = 0
|
||||||
|
except OperationalException as e:
|
||||||
|
logger.error(str(e))
|
||||||
|
return_code = 2
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.exception('Fatal exception!')
|
logger.exception('Fatal exception!')
|
||||||
finally:
|
finally:
|
||||||
@ -61,6 +65,7 @@ def set_loggers() -> None:
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
||||||
|
logging.getLogger('ccxt.base.exchange').setLevel(logging.INFO)
|
||||||
logging.getLogger('telegram').setLevel(logging.INFO)
|
logging.getLogger('telegram').setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ def file_dump_json(filename, data, is_zip=False) -> None:
|
|||||||
json.dump(data, fp, default=str)
|
json.dump(data, fp, default=str)
|
||||||
|
|
||||||
|
|
||||||
def format_ms_time(date: str) -> str:
|
def format_ms_time(date: int) -> str:
|
||||||
"""
|
"""
|
||||||
convert MS date to readable format.
|
convert MS date to readable format.
|
||||||
: epoch-string in ms
|
: epoch-string in ms
|
||||||
|
@ -4,44 +4,45 @@ import gzip
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from typing import Optional, List, Dict, Tuple, Any
|
||||||
import arrow
|
import arrow
|
||||||
from typing import Optional, List, Dict, Tuple
|
|
||||||
|
|
||||||
from freqtrade import misc, constants
|
from freqtrade import misc, constants
|
||||||
from freqtrade.exchange import get_ticker_history
|
from freqtrade.exchange import get_ticker_history
|
||||||
|
from freqtrade.arguments import TimeRange
|
||||||
|
|
||||||
from user_data.hyperopt_conf import hyperopt_optimize_conf
|
from user_data.hyperopt_conf import hyperopt_optimize_conf
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def trim_tickerlist(tickerlist: List[Dict], timerange: Tuple[Tuple, int, int]) -> List[Dict]:
|
def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
|
||||||
if not tickerlist:
|
if not tickerlist:
|
||||||
return tickerlist
|
return tickerlist
|
||||||
|
|
||||||
stype, start, stop = timerange
|
|
||||||
|
|
||||||
start_index = 0
|
start_index = 0
|
||||||
stop_index = len(tickerlist)
|
stop_index = len(tickerlist)
|
||||||
|
|
||||||
if stype[0] == 'line':
|
if timerange.starttype == 'line':
|
||||||
stop_index = start
|
stop_index = timerange.startts
|
||||||
if stype[0] == 'index':
|
if timerange.starttype == 'index':
|
||||||
start_index = start
|
start_index = timerange.startts
|
||||||
elif stype[0] == 'date':
|
elif timerange.starttype == 'date':
|
||||||
while tickerlist[start_index][0] < start * 1000:
|
while (start_index < len(tickerlist) and
|
||||||
|
tickerlist[start_index][0] < timerange.startts * 1000):
|
||||||
start_index += 1
|
start_index += 1
|
||||||
|
|
||||||
if stype[1] == 'line':
|
if timerange.stoptype == 'line':
|
||||||
start_index = len(tickerlist) + stop
|
start_index = len(tickerlist) + timerange.stopts
|
||||||
if stype[1] == 'index':
|
if timerange.stoptype == 'index':
|
||||||
stop_index = stop
|
stop_index = timerange.stopts
|
||||||
elif stype[1] == 'date':
|
elif timerange.stoptype == 'date':
|
||||||
while tickerlist[stop_index-1][0] > stop * 1000:
|
while (stop_index > 0 and
|
||||||
|
tickerlist[stop_index-1][0] > timerange.stopts * 1000):
|
||||||
stop_index -= 1
|
stop_index -= 1
|
||||||
|
|
||||||
if start_index > stop_index:
|
if start_index > stop_index:
|
||||||
raise ValueError(f'The timerange [{start},{stop}] is incorrect')
|
raise ValueError(f'The timerange [{timerange.startts},{timerange.stopts}] is incorrect')
|
||||||
|
|
||||||
return tickerlist[start_index:stop_index]
|
return tickerlist[start_index:stop_index]
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: Tuple[Tuple, int, int]) -
|
|||||||
def load_tickerdata_file(
|
def load_tickerdata_file(
|
||||||
datadir: str, pair: str,
|
datadir: str, pair: str,
|
||||||
ticker_interval: str,
|
ticker_interval: str,
|
||||||
timerange: Optional[Tuple[Tuple, int, int]] = None) -> Optional[List[Dict]]:
|
timerange: Optional[TimeRange] = None) -> Optional[List[Dict]]:
|
||||||
"""
|
"""
|
||||||
Load a pair from file,
|
Load a pair from file,
|
||||||
:return dict OR empty if unsuccesful
|
:return dict OR empty if unsuccesful
|
||||||
@ -84,7 +85,7 @@ def load_data(datadir: str,
|
|||||||
ticker_interval: str,
|
ticker_interval: str,
|
||||||
pairs: Optional[List[str]] = None,
|
pairs: Optional[List[str]] = None,
|
||||||
refresh_pairs: Optional[bool] = False,
|
refresh_pairs: Optional[bool] = False,
|
||||||
timerange: Optional[Tuple[Tuple, int, int]] = None) -> Dict[str, List]:
|
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> Dict[str, List]:
|
||||||
"""
|
"""
|
||||||
Loads ticker history data for the given parameters
|
Loads ticker history data for the given parameters
|
||||||
:return: dict
|
:return: dict
|
||||||
@ -100,15 +101,16 @@ def load_data(datadir: str,
|
|||||||
|
|
||||||
for pair in _pairs:
|
for pair in _pairs:
|
||||||
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
|
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
|
||||||
if not pairdata:
|
if pairdata:
|
||||||
# download the tickerdata from exchange
|
|
||||||
download_backtesting_testdata(datadir,
|
|
||||||
pair=pair,
|
|
||||||
tick_interval=ticker_interval,
|
|
||||||
timerange=timerange)
|
|
||||||
# and retry reading the pair
|
|
||||||
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
|
|
||||||
result[pair] = pairdata
|
result[pair] = pairdata
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
'No data for pair: "%s", Interval: %s. '
|
||||||
|
'Use --refresh-pairs-cached to download the data',
|
||||||
|
pair,
|
||||||
|
ticker_interval
|
||||||
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@ -123,7 +125,7 @@ def make_testdata_path(datadir: str) -> str:
|
|||||||
|
|
||||||
def download_pairs(datadir, pairs: List[str],
|
def download_pairs(datadir, pairs: List[str],
|
||||||
ticker_interval: str,
|
ticker_interval: str,
|
||||||
timerange: Optional[Tuple[Tuple, int, int]] = None) -> bool:
|
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> bool:
|
||||||
"""For each pairs passed in parameters, download the ticker intervals"""
|
"""For each pairs passed in parameters, download the ticker intervals"""
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
try:
|
try:
|
||||||
@ -143,7 +145,9 @@ def download_pairs(datadir, pairs: List[str],
|
|||||||
|
|
||||||
def load_cached_data_for_updating(filename: str,
|
def load_cached_data_for_updating(filename: str,
|
||||||
tick_interval: str,
|
tick_interval: str,
|
||||||
timerange: Optional[Tuple[Tuple, int, int]]) -> Tuple[list, int]:
|
timerange: Optional[TimeRange]) -> Tuple[
|
||||||
|
List[Any],
|
||||||
|
Optional[int]]:
|
||||||
"""
|
"""
|
||||||
Load cached data and choose what part of the data should be updated
|
Load cached data and choose what part of the data should be updated
|
||||||
"""
|
"""
|
||||||
@ -152,10 +156,10 @@ def load_cached_data_for_updating(filename: str,
|
|||||||
|
|
||||||
# user sets timerange, so find the start time
|
# user sets timerange, so find the start time
|
||||||
if timerange:
|
if timerange:
|
||||||
if timerange[0][0] == 'date':
|
if timerange.starttype == 'date':
|
||||||
since_ms = timerange[1] * 1000
|
since_ms = timerange.startts * 1000
|
||||||
elif timerange[0][1] == 'line':
|
elif timerange.stoptype == 'line':
|
||||||
num_minutes = timerange[2] * constants.TICKER_INTERVAL_MINUTES[tick_interval]
|
num_minutes = timerange.stopts * constants.TICKER_INTERVAL_MINUTES[tick_interval]
|
||||||
since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000
|
since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000
|
||||||
|
|
||||||
# read the cached file
|
# read the cached file
|
||||||
@ -185,7 +189,7 @@ def load_cached_data_for_updating(filename: str,
|
|||||||
def download_backtesting_testdata(datadir: str,
|
def download_backtesting_testdata(datadir: str,
|
||||||
pair: str,
|
pair: str,
|
||||||
tick_interval: str = '5m',
|
tick_interval: str = '5m',
|
||||||
timerange: Optional[Tuple[Tuple, int, int]] = None) -> None:
|
timerange: Optional[TimeRange] = None) -> None:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Download the latest ticker intervals from the exchange for the pairs passed in parameters
|
Download the latest ticker intervals from the exchange for the pairs passed in parameters
|
||||||
|
@ -33,18 +33,6 @@ class Backtesting(object):
|
|||||||
"""
|
"""
|
||||||
def __init__(self, config: Dict[str, Any]) -> None:
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.analyze = None
|
|
||||||
self.ticker_interval = None
|
|
||||||
self.tickerdata_to_dataframe = None
|
|
||||||
self.populate_buy_trend = None
|
|
||||||
self.populate_sell_trend = None
|
|
||||||
self._init()
|
|
||||||
|
|
||||||
def _init(self) -> None:
|
|
||||||
"""
|
|
||||||
Init objects required for backtesting
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.analyze = Analyze(self.config)
|
self.analyze = Analyze(self.config)
|
||||||
self.ticker_interval = self.analyze.strategy.ticker_interval
|
self.ticker_interval = self.analyze.strategy.ticker_interval
|
||||||
self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe
|
self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe
|
||||||
@ -78,7 +66,7 @@ class Backtesting(object):
|
|||||||
Generates and returns a text table for the given backtest data and the results dataframe
|
Generates and returns a text table for the given backtest data and the results dataframe
|
||||||
:return: pretty printed table with tabulate as str
|
:return: pretty printed table with tabulate as str
|
||||||
"""
|
"""
|
||||||
stake_currency = self.config.get('stake_currency')
|
stake_currency = str(self.config.get('stake_currency'))
|
||||||
|
|
||||||
floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
|
floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
|
||||||
tabular_data = []
|
tabular_data = []
|
||||||
@ -106,7 +94,7 @@ class Backtesting(object):
|
|||||||
len(results[results.profit_BTC > 0]),
|
len(results[results.profit_BTC > 0]),
|
||||||
len(results[results.profit_BTC < 0])
|
len(results[results.profit_BTC < 0])
|
||||||
])
|
])
|
||||||
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
|
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
|
||||||
|
|
||||||
def _get_sell_trade_entry(
|
def _get_sell_trade_entry(
|
||||||
self, pair: str, buy_row: DataFrame,
|
self, pair: str, buy_row: DataFrame,
|
||||||
@ -166,13 +154,22 @@ class Backtesting(object):
|
|||||||
max_open_trades = args.get('max_open_trades', 0)
|
max_open_trades = args.get('max_open_trades', 0)
|
||||||
realistic = args.get('realistic', False)
|
realistic = args.get('realistic', False)
|
||||||
record = args.get('record', None)
|
record = args.get('record', None)
|
||||||
|
recordfilename = args.get('recordfn', 'backtest-result.json')
|
||||||
records = []
|
records = []
|
||||||
trades = []
|
trades = []
|
||||||
trade_count_lock = {}
|
trade_count_lock: Dict = {}
|
||||||
for pair, pair_data in processed.items():
|
for pair, pair_data in processed.items():
|
||||||
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
|
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
|
||||||
|
|
||||||
ticker_data = self.populate_sell_trend(self.populate_buy_trend(pair_data))[headers]
|
ticker_data = self.populate_sell_trend(
|
||||||
|
self.populate_buy_trend(pair_data))[headers].copy()
|
||||||
|
|
||||||
|
# to avoid using data from future, we buy/sell with signal from previous candle
|
||||||
|
ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1)
|
||||||
|
ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1)
|
||||||
|
|
||||||
|
ticker_data.drop(ticker_data.head(1).index, inplace=True)
|
||||||
|
|
||||||
ticker = [x for x in ticker_data.itertuples()]
|
ticker = [x for x in ticker_data.itertuples()]
|
||||||
|
|
||||||
lock_pair_until = None
|
lock_pair_until = None
|
||||||
@ -208,8 +205,8 @@ class Backtesting(object):
|
|||||||
# For now export inside backtest(), maybe change so that backtest()
|
# For now export inside backtest(), maybe change so that backtest()
|
||||||
# returns a tuple like: (dataframe, records, logs, etc)
|
# returns a tuple like: (dataframe, records, logs, etc)
|
||||||
if record and record.find('trades') >= 0:
|
if record and record.find('trades') >= 0:
|
||||||
logger.info('Dumping backtest results')
|
logger.info('Dumping backtest results to %s', recordfilename)
|
||||||
file_dump_json('backtest-result.json', records)
|
file_dump_json(recordfilename, records)
|
||||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
||||||
return DataFrame.from_records(trades, columns=labels)
|
return DataFrame.from_records(trades, columns=labels)
|
||||||
|
|
||||||
@ -230,7 +227,8 @@ class Backtesting(object):
|
|||||||
else:
|
else:
|
||||||
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
||||||
|
|
||||||
timerange = Arguments.parse_timerange(self.config.get('timerange'))
|
timerange = Arguments.parse_timerange(None if self.config.get(
|
||||||
|
'timerange') is None else str(self.config.get('timerange')))
|
||||||
data = optimize.load_data(
|
data = optimize.load_data(
|
||||||
self.config['datadir'],
|
self.config['datadir'],
|
||||||
pairs=pairs,
|
pairs=pairs,
|
||||||
@ -268,7 +266,8 @@ class Backtesting(object):
|
|||||||
'realistic': self.config.get('realistic_simulation', False),
|
'realistic': self.config.get('realistic_simulation', False),
|
||||||
'sell_profit_only': sell_profit_only,
|
'sell_profit_only': sell_profit_only,
|
||||||
'use_sell_signal': use_sell_signal,
|
'use_sell_signal': use_sell_signal,
|
||||||
'record': self.config.get('export')
|
'record': self.config.get('export'),
|
||||||
|
'recordfn': self.config.get('exportfilename'),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
|
@ -14,7 +14,7 @@ from argparse import Namespace
|
|||||||
from functools import reduce
|
from functools import reduce
|
||||||
from math import exp
|
from math import exp
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from typing import Dict, Any, Callable
|
from typing import Dict, Any, Callable, Optional
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
@ -60,7 +60,7 @@ class Hyperopt(Backtesting):
|
|||||||
self.expected_max_profit = 3.0
|
self.expected_max_profit = 3.0
|
||||||
|
|
||||||
# Configuration and data used by hyperopt
|
# Configuration and data used by hyperopt
|
||||||
self.processed = None
|
self.processed: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
# Hyperopt Trials
|
# Hyperopt Trials
|
||||||
self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle')
|
self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle')
|
||||||
@ -344,7 +344,7 @@ class Hyperopt(Backtesting):
|
|||||||
"""
|
"""
|
||||||
Return the space to use during Hyperopt
|
Return the space to use during Hyperopt
|
||||||
"""
|
"""
|
||||||
spaces = {}
|
spaces: Dict = {}
|
||||||
if self.has_space('buy'):
|
if self.has_space('buy'):
|
||||||
spaces = {**spaces, **Hyperopt.indicator_space()}
|
spaces = {**spaces, **Hyperopt.indicator_space()}
|
||||||
if self.has_space('roi'):
|
if self.has_space('roi'):
|
||||||
@ -455,6 +455,7 @@ class Hyperopt(Backtesting):
|
|||||||
|
|
||||||
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
|
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
|
||||||
print('.', end='')
|
print('.', end='')
|
||||||
|
sys.stdout.flush()
|
||||||
return {
|
return {
|
||||||
'status': STATUS_FAIL,
|
'status': STATUS_FAIL,
|
||||||
'loss': float('inf')
|
'loss': float('inf')
|
||||||
@ -479,31 +480,32 @@ class Hyperopt(Backtesting):
|
|||||||
'result': result_explanation,
|
'result': result_explanation,
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
def format_results(self, results: DataFrame) -> str:
|
||||||
def format_results(results: DataFrame) -> str:
|
|
||||||
"""
|
"""
|
||||||
Return the format result in a string
|
Return the format result in a string
|
||||||
"""
|
"""
|
||||||
return ('{:6d} trades. Avg profit {: 5.2f}%. '
|
return ('{:6d} trades. Avg profit {: 5.2f}%. '
|
||||||
'Total profit {: 11.8f} BTC ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
|
'Total profit {: 11.8f} {} ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
|
||||||
len(results.index),
|
len(results.index),
|
||||||
results.profit_percent.mean() * 100.0,
|
results.profit_percent.mean() * 100.0,
|
||||||
results.profit_BTC.sum(),
|
results.profit_BTC.sum(),
|
||||||
|
self.config['stake_currency'],
|
||||||
results.profit_percent.sum(),
|
results.profit_percent.sum(),
|
||||||
results.duration.mean(),
|
results.duration.mean(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
timerange = Arguments.parse_timerange(self.config.get('timerange'))
|
timerange = Arguments.parse_timerange(None if self.config.get(
|
||||||
|
'timerange') is None else str(self.config.get('timerange')))
|
||||||
data = load_data(
|
data = load_data(
|
||||||
datadir=self.config.get('datadir'),
|
datadir=str(self.config.get('datadir')),
|
||||||
pairs=self.config['exchange']['pair_whitelist'],
|
pairs=self.config['exchange']['pair_whitelist'],
|
||||||
ticker_interval=self.ticker_interval,
|
ticker_interval=self.ticker_interval,
|
||||||
timerange=timerange
|
timerange=timerange
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.has_space('buy'):
|
if self.has_space('buy'):
|
||||||
self.analyze.populate_indicators = Hyperopt.populate_indicators
|
self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore
|
||||||
self.processed = self.tickerdata_to_dataframe(data)
|
self.processed = self.tickerdata_to_dataframe(data)
|
||||||
|
|
||||||
if self.config.get('mongodb'):
|
if self.config.get('mongodb'):
|
||||||
|
@ -5,48 +5,54 @@ This module contains the class to persist trades into SQLite
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal, getcontext
|
from decimal import Decimal, getcontext
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional, Any
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
|
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
|
||||||
create_engine)
|
create_engine)
|
||||||
from sqlalchemy.engine import Engine
|
from sqlalchemy import inspect
|
||||||
|
from sqlalchemy.exc import NoSuchModuleError
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm.scoping import scoped_session
|
from sqlalchemy.orm.scoping import scoped_session
|
||||||
from sqlalchemy.orm.session import sessionmaker
|
from sqlalchemy.orm.session import sessionmaker
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
from sqlalchemy import inspect
|
|
||||||
|
|
||||||
|
from freqtrade import OperationalException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_CONF = {}
|
_CONF = {}
|
||||||
_DECL_BASE = declarative_base()
|
_DECL_BASE: Any = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
def init(config: dict, engine: Optional[Engine] = None) -> None:
|
def init(config: Dict) -> None:
|
||||||
"""
|
"""
|
||||||
Initializes this module with the given config,
|
Initializes this module with the given config,
|
||||||
registers all known command handlers
|
registers all known command handlers
|
||||||
and starts polling for message updates
|
and starts polling for message updates
|
||||||
:param config: config to use
|
:param config: config to use
|
||||||
:param engine: database engine for sqlalchemy (Optional)
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
_CONF.update(config)
|
_CONF.update(config)
|
||||||
if not engine:
|
|
||||||
if _CONF.get('dry_run', False):
|
db_url = _CONF.get('db_url', None)
|
||||||
# the user wants dry run to use a DB
|
kwargs = {}
|
||||||
if _CONF.get('dry_run_db', False):
|
|
||||||
engine = create_engine('sqlite:///tradesv3.dry_run.sqlite')
|
# Take care of thread ownership if in-memory db
|
||||||
# Otherwise dry run will store in memory
|
if db_url == 'sqlite://':
|
||||||
else:
|
kwargs.update({
|
||||||
engine = create_engine('sqlite://',
|
'connect_args': {'check_same_thread': False},
|
||||||
connect_args={'check_same_thread': False},
|
'poolclass': StaticPool,
|
||||||
poolclass=StaticPool,
|
'echo': False,
|
||||||
echo=False)
|
})
|
||||||
else:
|
|
||||||
engine = create_engine('sqlite:///tradesv3.sqlite')
|
try:
|
||||||
|
engine = create_engine(db_url, **kwargs)
|
||||||
|
except NoSuchModuleError:
|
||||||
|
error = 'Given value for db_url: \'{}\' is no valid database URL! (See {}).'.format(
|
||||||
|
db_url, 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
|
||||||
|
)
|
||||||
|
raise OperationalException(error)
|
||||||
|
|
||||||
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
||||||
Trade.session = session()
|
Trade.session = session()
|
||||||
@ -54,8 +60,8 @@ def init(config: dict, engine: Optional[Engine] = None) -> None:
|
|||||||
_DECL_BASE.metadata.create_all(engine)
|
_DECL_BASE.metadata.create_all(engine)
|
||||||
check_migrate(engine)
|
check_migrate(engine)
|
||||||
|
|
||||||
# Clean dry_run DB
|
# Clean dry_run DB if the db is not in-memory
|
||||||
if _CONF.get('dry_run', False) and _CONF.get('dry_run_db', False):
|
if _CONF.get('dry_run', False) and db_url != 'sqlite://':
|
||||||
clean_dry_run_db()
|
clean_dry_run_db()
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,13 +2,14 @@
|
|||||||
This module contains class to define a RPC communications
|
This module contains class to define a RPC communications
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Tuple, Any
|
from typing import Dict, Tuple, Any
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import sqlalchemy as sql
|
import sqlalchemy as sql
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
from numpy import mean, nan_to_num
|
||||||
|
|
||||||
from freqtrade import exchange
|
from freqtrade import exchange
|
||||||
from freqtrade.misc import shorten_date
|
from freqtrade.misc import shorten_date
|
||||||
@ -114,7 +115,7 @@ class RPC(object):
|
|||||||
self, timescale: int,
|
self, timescale: int,
|
||||||
stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]:
|
stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]:
|
||||||
today = datetime.utcnow().date()
|
today = datetime.utcnow().date()
|
||||||
profit_days = {}
|
profit_days: Dict[date, Dict] = {}
|
||||||
|
|
||||||
if not (isinstance(timescale, int) and timescale > 0):
|
if not (isinstance(timescale, int) and timescale > 0):
|
||||||
return True, '*Daily [n]:* `must be an integer greater than 0`'
|
return True, '*Daily [n]:* `must be an integer greater than 0`'
|
||||||
@ -172,7 +173,7 @@ class RPC(object):
|
|||||||
durations = []
|
durations = []
|
||||||
|
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
current_rate = None
|
current_rate: float = 0.0
|
||||||
|
|
||||||
if not trade.open_rate:
|
if not trade.open_rate:
|
||||||
continue
|
continue
|
||||||
@ -209,14 +210,14 @@ class RPC(object):
|
|||||||
fiat = self.freqtrade.fiat_converter
|
fiat = self.freqtrade.fiat_converter
|
||||||
# Prepare data to display
|
# Prepare data to display
|
||||||
profit_closed_coin = round(sum(profit_closed_coin), 8)
|
profit_closed_coin = round(sum(profit_closed_coin), 8)
|
||||||
profit_closed_percent = round(sum(profit_closed_percent) * 100, 2)
|
profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2)
|
||||||
profit_closed_fiat = fiat.convert_amount(
|
profit_closed_fiat = fiat.convert_amount(
|
||||||
profit_closed_coin,
|
profit_closed_coin,
|
||||||
stake_currency,
|
stake_currency,
|
||||||
fiat_display_currency
|
fiat_display_currency
|
||||||
)
|
)
|
||||||
profit_all_coin = round(sum(profit_all_coin), 8)
|
profit_all_coin = round(sum(profit_all_coin), 8)
|
||||||
profit_all_percent = round(sum(profit_all_percent) * 100, 2)
|
profit_all_percent = round(nan_to_num(mean(profit_all_percent)) * 100, 2)
|
||||||
profit_all_fiat = fiat.convert_amount(
|
profit_all_fiat = fiat.convert_amount(
|
||||||
profit_all_coin,
|
profit_all_coin,
|
||||||
stake_currency,
|
stake_currency,
|
||||||
@ -278,7 +279,7 @@ class RPC(object):
|
|||||||
value = fiat.convert_amount(total, 'BTC', symbol)
|
value = fiat.convert_amount(total, 'BTC', symbol)
|
||||||
return False, (output, total, symbol, value)
|
return False, (output, total, symbol, value)
|
||||||
|
|
||||||
def rpc_start(self) -> (bool, str):
|
def rpc_start(self) -> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
Handler for start.
|
Handler for start.
|
||||||
"""
|
"""
|
||||||
@ -288,7 +289,7 @@ class RPC(object):
|
|||||||
self.freqtrade.state = State.RUNNING
|
self.freqtrade.state = State.RUNNING
|
||||||
return False, '`Starting trader ...`'
|
return False, '`Starting trader ...`'
|
||||||
|
|
||||||
def rpc_stop(self) -> (bool, str):
|
def rpc_stop(self) -> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
Handler for stop.
|
Handler for stop.
|
||||||
"""
|
"""
|
||||||
@ -316,8 +317,10 @@ class RPC(object):
|
|||||||
and order['side'] == 'buy':
|
and order['side'] == 'buy':
|
||||||
exchange.cancel_order(trade.open_order_id, trade.pair)
|
exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||||
trade.close(order.get('price') or trade.open_rate)
|
trade.close(order.get('price') or trade.open_rate)
|
||||||
# TODO: sell amount which has been bought already
|
# Do the best effort, if we don't know 'filled' amount, don't try selling
|
||||||
|
if order['filled'] is None:
|
||||||
return
|
return
|
||||||
|
trade.amount = order['filled']
|
||||||
|
|
||||||
# Ignore trades with an attached LIMIT_SELL order
|
# Ignore trades with an attached LIMIT_SELL order
|
||||||
if order and order['status'] == 'open' \
|
if order and order['status'] == 'open' \
|
||||||
@ -351,6 +354,7 @@ class RPC(object):
|
|||||||
return True, 'Invalid argument.'
|
return True, 'Invalid argument.'
|
||||||
|
|
||||||
_exec_forcesell(trade)
|
_exec_forcesell(trade)
|
||||||
|
Trade.session.flush()
|
||||||
return False, ''
|
return False, ''
|
||||||
|
|
||||||
def rpc_performance(self) -> Tuple[bool, Any]:
|
def rpc_performance(self) -> Tuple[bool, Any]:
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
This module contains class to manage RPC communications (Telegram, Slack, ...)
|
This module contains class to manage RPC communications (Telegram, Slack, ...)
|
||||||
"""
|
"""
|
||||||
|
from typing import Any, List
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from freqtrade.rpc.telegram import Telegram
|
from freqtrade.rpc.telegram import Telegram
|
||||||
@ -21,8 +22,8 @@ class RPCManager(object):
|
|||||||
"""
|
"""
|
||||||
self.freqtrade = freqtrade
|
self.freqtrade = freqtrade
|
||||||
|
|
||||||
self.registered_modules = []
|
self.registered_modules: List[str] = []
|
||||||
self.telegram = None
|
self.telegram: Any = None
|
||||||
self._init()
|
self._init()
|
||||||
|
|
||||||
def _init(self) -> None:
|
def _init(self) -> None:
|
||||||
|
@ -18,7 +18,7 @@ from freqtrade.rpc.rpc import RPC
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
|
def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Callable[..., Any]:
|
||||||
"""
|
"""
|
||||||
Decorator to check if the message comes from the correct chat_id
|
Decorator to check if the message comes from the correct chat_id
|
||||||
:param command_handler: Telegram CommandHandler
|
:param command_handler: Telegram CommandHandler
|
||||||
@ -65,7 +65,7 @@ class Telegram(RPC):
|
|||||||
"""
|
"""
|
||||||
super().__init__(freqtrade)
|
super().__init__(freqtrade)
|
||||||
|
|
||||||
self._updater = None
|
self._updater: Updater = None
|
||||||
self._config = freqtrade.config
|
self._config = freqtrade.config
|
||||||
self._init()
|
self._init()
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
IStrategy interface
|
IStrategy interface
|
||||||
This module defines the interface to apply for strategies
|
This module defines the interface to apply for strategies
|
||||||
"""
|
"""
|
||||||
|
from typing import Dict
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
@ -16,9 +16,13 @@ class IStrategy(ABC):
|
|||||||
Attributes you can use:
|
Attributes you can use:
|
||||||
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
||||||
stoploss -> float: optimal stoploss designed for the strategy
|
stoploss -> float: optimal stoploss designed for the strategy
|
||||||
ticker_interval -> int: value of the ticker interval to use for the strategy
|
ticker_interval -> str: value of the ticker interval to use for the strategy
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
minimal_roi: Dict
|
||||||
|
stoploss: float
|
||||||
|
ticker_interval: str
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
|
@ -33,7 +33,8 @@ class StrategyResolver(object):
|
|||||||
|
|
||||||
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
|
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
|
||||||
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY
|
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY
|
||||||
self.strategy = self._load_strategy(strategy_name, extra_dir=config.get('strategy_path'))
|
self.strategy: IStrategy = self._load_strategy(strategy_name,
|
||||||
|
extra_dir=config.get('strategy_path'))
|
||||||
|
|
||||||
# Set attributes
|
# Set attributes
|
||||||
# Check if we need to override configuration
|
# Check if we need to override configuration
|
||||||
@ -61,7 +62,7 @@ class StrategyResolver(object):
|
|||||||
self.strategy.stoploss = float(self.strategy.stoploss)
|
self.strategy.stoploss = float(self.strategy.stoploss)
|
||||||
|
|
||||||
def _load_strategy(
|
def _load_strategy(
|
||||||
self, strategy_name: str, extra_dir: Optional[str] = None) -> Optional[IStrategy]:
|
self, strategy_name: str, extra_dir: Optional[str] = None) -> IStrategy:
|
||||||
"""
|
"""
|
||||||
Search and loads the specified strategy.
|
Search and loads the specified strategy.
|
||||||
:param strategy_name: name of the module to import
|
:param strategy_name: name of the module to import
|
||||||
@ -101,7 +102,7 @@ class StrategyResolver(object):
|
|||||||
# Generate spec based on absolute path
|
# Generate spec based on absolute path
|
||||||
spec = importlib.util.spec_from_file_location('user_data.strategies', module_path)
|
spec = importlib.util.spec_from_file_location('user_data.strategies', module_path)
|
||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(module)
|
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
|
||||||
|
|
||||||
valid_strategies_gen = (
|
valid_strategies_gen = (
|
||||||
obj for name, obj in inspect.getmembers(module, inspect.isclass)
|
obj for name, obj in inspect.getmembers(module, inspect.isclass)
|
||||||
|
@ -9,7 +9,6 @@ from unittest.mock import MagicMock
|
|||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from telegram import Chat, Message, Update
|
from telegram import Chat, Message, Update
|
||||||
|
|
||||||
from freqtrade.analyze import Analyze
|
from freqtrade.analyze import Analyze
|
||||||
@ -45,7 +44,7 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
|||||||
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
|
||||||
mocker.patch('freqtrade.freqtradebot.Analyze.get_signal', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.Analyze.get_signal', MagicMock())
|
||||||
|
|
||||||
return FreqtradeBot(config, create_engine('sqlite://'))
|
return FreqtradeBot(config)
|
||||||
|
|
||||||
|
|
||||||
def patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> None:
|
def patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> None:
|
||||||
@ -88,7 +87,14 @@ def default_conf():
|
|||||||
"stoploss": -0.10,
|
"stoploss": -0.10,
|
||||||
"unfilledtimeout": 600,
|
"unfilledtimeout": 600,
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0
|
"use_book_order": False,
|
||||||
|
"book_order_top": 6,
|
||||||
|
"ask_last_balance": 0.0,
|
||||||
|
},
|
||||||
|
"ask_strategy": {
|
||||||
|
"use_book_order": False,
|
||||||
|
"book_order_min": 1,
|
||||||
|
"book_order_max": 10
|
||||||
},
|
},
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
@ -108,7 +114,8 @@ def default_conf():
|
|||||||
"chat_id": "0"
|
"chat_id": "0"
|
||||||
},
|
},
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"loglevel": logging.DEBUG
|
"db_url": "sqlite://",
|
||||||
|
"loglevel": logging.DEBUG,
|
||||||
}
|
}
|
||||||
validate(configuration, constants.CONF_SCHEMA)
|
validate(configuration, constants.CONF_SCHEMA)
|
||||||
return configuration
|
return configuration
|
||||||
|
@ -310,9 +310,19 @@ def test_get_ticker(default_conf, mocker):
|
|||||||
# if not fetching a new result we should get the cached ticker
|
# if not fetching a new result we should get the cached ticker
|
||||||
ticker = get_ticker(pair='ETH/BTC')
|
ticker = get_ticker(pair='ETH/BTC')
|
||||||
|
|
||||||
|
assert api_mock.fetch_ticker.call_count == 1
|
||||||
assert ticker['bid'] == 0.5
|
assert ticker['bid'] == 0.5
|
||||||
assert ticker['ask'] == 1
|
assert ticker['ask'] == 1
|
||||||
|
|
||||||
|
assert 'ETH/BTC' in exchange._CACHED_TICKER
|
||||||
|
assert exchange._CACHED_TICKER['ETH/BTC']['bid'] == 0.5
|
||||||
|
assert exchange._CACHED_TICKER['ETH/BTC']['ask'] == 1
|
||||||
|
|
||||||
|
# Test caching
|
||||||
|
api_mock.fetch_ticker = MagicMock()
|
||||||
|
get_ticker(pair='ETH/BTC', refresh=False)
|
||||||
|
assert api_mock.fetch_ticker.call_count == 0
|
||||||
|
|
||||||
with pytest.raises(TemporaryError): # test retrier
|
with pytest.raises(TemporaryError): # test retrier
|
||||||
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.NetworkError)
|
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
@ -323,6 +333,10 @@ def test_get_ticker(default_conf, mocker):
|
|||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
get_ticker(pair='ETH/BTC', refresh=True)
|
get_ticker(pair='ETH/BTC', refresh=True)
|
||||||
|
|
||||||
|
api_mock.fetch_ticker = MagicMock(return_value={})
|
||||||
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
|
get_ticker(pair='ETH/BTC', refresh=True)
|
||||||
|
|
||||||
|
|
||||||
def make_fetch_ohlcv_mock(data):
|
def make_fetch_ohlcv_mock(data):
|
||||||
def fetch_ohlcv_mock(pair, timeframe, since):
|
def fetch_ohlcv_mock(pair, timeframe, since):
|
||||||
@ -393,6 +407,78 @@ def test_get_ticker_history(default_conf, mocker):
|
|||||||
get_ticker_history('EFGH/BTC', default_conf['ticker_interval'])
|
get_ticker_history('EFGH/BTC', default_conf['ticker_interval'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_ticker_history_sort(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
|
||||||
|
# GDAX use-case (real data from GDAX)
|
||||||
|
# This ticker history is ordered DESC (newest first, oldest last)
|
||||||
|
tick = [
|
||||||
|
[1527833100000, 0.07666, 0.07671, 0.07666, 0.07668, 16.65244264],
|
||||||
|
[1527832800000, 0.07662, 0.07666, 0.07662, 0.07666, 1.30051526],
|
||||||
|
[1527832500000, 0.07656, 0.07661, 0.07656, 0.07661, 12.034778840000001],
|
||||||
|
[1527832200000, 0.07658, 0.07658, 0.07655, 0.07656, 0.59780186],
|
||||||
|
[1527831900000, 0.07658, 0.07658, 0.07658, 0.07658, 1.76278136],
|
||||||
|
[1527831600000, 0.07658, 0.07658, 0.07658, 0.07658, 2.22646521],
|
||||||
|
[1527831300000, 0.07655, 0.07657, 0.07655, 0.07657, 1.1753],
|
||||||
|
[1527831000000, 0.07654, 0.07654, 0.07651, 0.07651, 0.8073060299999999],
|
||||||
|
[1527830700000, 0.07652, 0.07652, 0.07651, 0.07652, 10.04822687],
|
||||||
|
[1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867]
|
||||||
|
]
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
|
||||||
|
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
|
||||||
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
|
|
||||||
|
# Test the ticker history sort
|
||||||
|
ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
|
||||||
|
assert ticks[0][0] == 1527830400000
|
||||||
|
assert ticks[0][1] == 0.07649
|
||||||
|
assert ticks[0][2] == 0.07651
|
||||||
|
assert ticks[0][3] == 0.07649
|
||||||
|
assert ticks[0][4] == 0.07651
|
||||||
|
assert ticks[0][5] == 2.5734867
|
||||||
|
|
||||||
|
assert ticks[9][0] == 1527833100000
|
||||||
|
assert ticks[9][1] == 0.07666
|
||||||
|
assert ticks[9][2] == 0.07671
|
||||||
|
assert ticks[9][3] == 0.07666
|
||||||
|
assert ticks[9][4] == 0.07668
|
||||||
|
assert ticks[9][5] == 16.65244264
|
||||||
|
|
||||||
|
# Bittrex use-case (real data from Bittrex)
|
||||||
|
# This ticker history is ordered ASC (oldest first, newest last)
|
||||||
|
tick = [
|
||||||
|
[1527827700000, 0.07659999, 0.0766, 0.07627, 0.07657998, 1.85216924],
|
||||||
|
[1527828000000, 0.07657995, 0.07657995, 0.0763, 0.0763, 26.04051037],
|
||||||
|
[1527828300000, 0.0763, 0.07659998, 0.0763, 0.0764, 10.36434124],
|
||||||
|
[1527828600000, 0.0764, 0.0766, 0.0764, 0.0766, 5.71044773],
|
||||||
|
[1527828900000, 0.0764, 0.07666998, 0.0764, 0.07666998, 47.48888565],
|
||||||
|
[1527829200000, 0.0765, 0.07672999, 0.0765, 0.07672999, 3.37640326],
|
||||||
|
[1527829500000, 0.0766, 0.07675, 0.0765, 0.07675, 8.36203831],
|
||||||
|
[1527829800000, 0.07675, 0.07677999, 0.07620002, 0.076695, 119.22963884],
|
||||||
|
[1527830100000, 0.076695, 0.07671, 0.07624171, 0.07671, 1.80689244],
|
||||||
|
[1527830400000, 0.07671, 0.07674399, 0.07629216, 0.07655213, 2.31452783]
|
||||||
|
]
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
|
||||||
|
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
|
||||||
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
|
|
||||||
|
# Test the ticker history sort
|
||||||
|
ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
|
||||||
|
assert ticks[0][0] == 1527827700000
|
||||||
|
assert ticks[0][1] == 0.07659999
|
||||||
|
assert ticks[0][2] == 0.0766
|
||||||
|
assert ticks[0][3] == 0.07627
|
||||||
|
assert ticks[0][4] == 0.07657998
|
||||||
|
assert ticks[0][5] == 1.85216924
|
||||||
|
|
||||||
|
assert ticks[9][0] == 1527830400000
|
||||||
|
assert ticks[9][1] == 0.07671
|
||||||
|
assert ticks[9][2] == 0.07674399
|
||||||
|
assert ticks[9][3] == 0.07629216
|
||||||
|
assert ticks[9][4] == 0.07655213
|
||||||
|
assert ticks[9][5] == 2.31452783
|
||||||
|
|
||||||
|
|
||||||
def test_cancel_order_dry_run(default_conf, mocker):
|
def test_cancel_order_dry_run(default_conf, mocker):
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||||
|
@ -13,7 +13,7 @@ from arrow import Arrow
|
|||||||
|
|
||||||
from freqtrade import optimize
|
from freqtrade import optimize
|
||||||
from freqtrade.analyze import Analyze
|
from freqtrade.analyze import Analyze
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments, TimeRange
|
||||||
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration
|
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ def trim_dictlist(dict_list, num):
|
|||||||
|
|
||||||
|
|
||||||
def load_data_test(what):
|
def load_data_test(what):
|
||||||
timerange = ((None, 'line'), None, -100)
|
timerange = TimeRange(None, 'line', 0, -101)
|
||||||
data = optimize.load_data(None, ticker_interval='1m',
|
data = optimize.load_data(None, ticker_interval='1m',
|
||||||
pairs=['UNITTEST/BTC'], timerange=timerange)
|
pairs=['UNITTEST/BTC'], timerange=timerange)
|
||||||
pair = data['UNITTEST/BTC']
|
pair = data['UNITTEST/BTC']
|
||||||
@ -110,14 +110,14 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals
|
|||||||
# use for mock freqtrade.exchange.get_ticker_history'
|
# use for mock freqtrade.exchange.get_ticker_history'
|
||||||
def _load_pair_as_ticks(pair, tickfreq):
|
def _load_pair_as_ticks(pair, tickfreq):
|
||||||
ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair])
|
ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair])
|
||||||
ticks = trim_dictlist(ticks, -200)
|
ticks = trim_dictlist(ticks, -201)
|
||||||
return ticks[pair]
|
return ticks[pair]
|
||||||
|
|
||||||
|
|
||||||
# FIX: fixturize this?
|
# FIX: fixturize this?
|
||||||
def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None):
|
def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None):
|
||||||
data = optimize.load_data(None, ticker_interval='8m', pairs=[pair])
|
data = optimize.load_data(None, ticker_interval='8m', pairs=[pair])
|
||||||
data = trim_dictlist(data, -200)
|
data = trim_dictlist(data, -201)
|
||||||
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
|
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
|
||||||
backtesting = Backtesting(conf)
|
backtesting = Backtesting(conf)
|
||||||
return {
|
return {
|
||||||
@ -181,7 +181,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
|||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
'Using data folder: {} ...'.format(config['datadir']),
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
@ -218,7 +218,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
'--realistic-simulation',
|
'--realistic-simulation',
|
||||||
'--refresh-pairs-cached',
|
'--refresh-pairs-cached',
|
||||||
'--timerange', ':100',
|
'--timerange', ':100',
|
||||||
'--export', '/bar/foo'
|
'--export', '/bar/foo',
|
||||||
|
'--export-filename', 'foo_bar.json'
|
||||||
]
|
]
|
||||||
|
|
||||||
config = setup_configuration(get_args(args))
|
config = setup_configuration(get_args(args))
|
||||||
@ -229,7 +230,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
'Using data folder: {} ...'.format(config['datadir']),
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
@ -259,6 +260,11 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
'Parameter --export detected: {} ...'.format(config['export']),
|
'Parameter --export detected: {} ...'.format(config['export']),
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
|
assert 'exportfilename' in config
|
||||||
|
assert log_has(
|
||||||
|
'Storing backtest results to {} ...'.format(config['exportfilename']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_start(mocker, fee, default_conf, caplog) -> None:
|
def test_start(mocker, fee, default_conf, caplog) -> None:
|
||||||
@ -286,23 +292,6 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
|
|||||||
assert start_mock.call_count == 1
|
assert start_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_backtesting__init__(mocker, default_conf) -> None:
|
|
||||||
"""
|
|
||||||
Test Backtesting.__init__() method
|
|
||||||
"""
|
|
||||||
init_mock = MagicMock()
|
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting._init', init_mock)
|
|
||||||
|
|
||||||
backtesting = Backtesting(default_conf)
|
|
||||||
assert backtesting.config == default_conf
|
|
||||||
assert backtesting.analyze is None
|
|
||||||
assert backtesting.ticker_interval is None
|
|
||||||
assert backtesting.tickerdata_to_dataframe is None
|
|
||||||
assert backtesting.populate_buy_trend is None
|
|
||||||
assert backtesting.populate_sell_trend is None
|
|
||||||
assert init_mock.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_backtesting_init(mocker, default_conf) -> None:
|
def test_backtesting_init(mocker, default_conf) -> None:
|
||||||
"""
|
"""
|
||||||
Test Backtesting._init() method
|
Test Backtesting._init() method
|
||||||
@ -322,13 +311,13 @@ def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
|
|||||||
Test Backtesting.tickerdata_to_dataframe() method
|
Test Backtesting.tickerdata_to_dataframe() method
|
||||||
"""
|
"""
|
||||||
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
|
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
|
||||||
timerange = ((None, 'line'), None, -100)
|
timerange = TimeRange(None, 'line', 0, -100)
|
||||||
tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
tickerlist = {'UNITTEST/BTC': tick}
|
tickerlist = {'UNITTEST/BTC': tick}
|
||||||
|
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
data = backtesting.tickerdata_to_dataframe(tickerlist)
|
data = backtesting.tickerdata_to_dataframe(tickerlist)
|
||||||
assert len(data['UNITTEST/BTC']) == 100
|
assert len(data['UNITTEST/BTC']) == 99
|
||||||
|
|
||||||
# Load Analyze to compare the result between Backtesting function and Analyze are the same
|
# Load Analyze to compare the result between Backtesting function and Analyze are the same
|
||||||
analyze = Analyze(default_conf)
|
analyze = Analyze(default_conf)
|
||||||
@ -352,7 +341,7 @@ def test_get_timeframe(default_conf, mocker) -> None:
|
|||||||
)
|
)
|
||||||
min_date, max_date = backtesting.get_timeframe(data)
|
min_date, max_date = backtesting.get_timeframe(data)
|
||||||
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
|
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
|
||||||
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
|
assert max_date.isoformat() == '2017-11-14T22:58:00+00:00'
|
||||||
|
|
||||||
|
|
||||||
def test_generate_text_table(default_conf, mocker):
|
def test_generate_text_table(default_conf, mocker):
|
||||||
@ -374,16 +363,15 @@ def test_generate_text_table(default_conf, mocker):
|
|||||||
)
|
)
|
||||||
|
|
||||||
result_str = (
|
result_str = (
|
||||||
'pair buy count avg profit % '
|
'| pair | buy count | avg profit % | '
|
||||||
'total profit BTC avg duration profit loss\n'
|
'total profit BTC | avg duration | profit | loss |\n'
|
||||||
'------- ----------- -------------- '
|
'|:--------|------------:|---------------:|'
|
||||||
'------------------ -------------- -------- ------\n'
|
'-------------------:|---------------:|---------:|-------:|\n'
|
||||||
'ETH/BTC 2 15.00 '
|
'| ETH/BTC | 2 | 15.00 | '
|
||||||
'0.60000000 20.0 2 0\n'
|
'0.60000000 | 20.0 | 2 | 0 |\n'
|
||||||
'TOTAL 2 15.00 '
|
'| TOTAL | 2 | 15.00 | '
|
||||||
'0.60000000 20.0 2 0'
|
'0.60000000 | 20.0 | 2 | 0 |'
|
||||||
)
|
)
|
||||||
|
|
||||||
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
|
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
|
||||||
|
|
||||||
|
|
||||||
@ -490,7 +478,7 @@ def test_processed(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
|
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
|
||||||
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
|
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
|
||||||
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
|
tests = [['raise', 17], ['lower', 0], ['sine', 16]]
|
||||||
for [contour, numres] in tests:
|
for [contour, numres] in tests:
|
||||||
simple_backtest(default_conf, contour, numres, mocker)
|
simple_backtest(default_conf, contour, numres, mocker)
|
||||||
|
|
||||||
@ -618,10 +606,12 @@ def test_backtest_start_live(default_conf, mocker, caplog):
|
|||||||
args = [
|
args = [
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--strategy', 'DefaultStrategy',
|
'--strategy', 'DefaultStrategy',
|
||||||
|
'--datadir', 'freqtrade/tests/testdata',
|
||||||
'backtesting',
|
'backtesting',
|
||||||
'--ticker-interval', '1m',
|
'--ticker-interval', '1m',
|
||||||
'--live',
|
'--live',
|
||||||
'--timerange', '-100'
|
'--timerange', '-100',
|
||||||
|
'--realistic-simulation'
|
||||||
]
|
]
|
||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
start(args)
|
start(args)
|
||||||
@ -631,13 +621,14 @@ def test_backtest_start_live(default_conf, mocker, caplog):
|
|||||||
'Using ticker_interval: 1m ...',
|
'Using ticker_interval: 1m ...',
|
||||||
'Parameter -l/--live detected ...',
|
'Parameter -l/--live detected ...',
|
||||||
'Using max_open_trades: 1 ...',
|
'Using max_open_trades: 1 ...',
|
||||||
'Parameter --timerange detected: -100 ..',
|
'Parameter --timerange detected: -100 ...',
|
||||||
'Parameter --datadir detected: freqtrade/tests/testdata ...',
|
'Using data folder: freqtrade/tests/testdata ...',
|
||||||
'Using stake_currency: BTC ...',
|
'Using stake_currency: BTC ...',
|
||||||
'Using stake_amount: 0.001 ...',
|
'Using stake_amount: 0.001 ...',
|
||||||
'Downloading data for all pairs in whitelist ...',
|
'Downloading data for all pairs in whitelist ...',
|
||||||
'Measuring data from 2017-11-14T19:32:00+00:00 up to 2017-11-14T22:59:00+00:00 (0 days)..'
|
'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..',
|
||||||
|
'Parameter --realistic-simulation detected ...'
|
||||||
]
|
]
|
||||||
|
|
||||||
for line in exists:
|
for line in exists:
|
||||||
log_has(line, caplog.record_tuples)
|
assert log_has(line, caplog.record_tuples)
|
||||||
|
@ -389,10 +389,12 @@ def test_start_uses_mongotrials(mocker, init_hyperopt, default_conf) -> None:
|
|||||||
# test buy_strategy_generator def populate_buy_trend
|
# test buy_strategy_generator def populate_buy_trend
|
||||||
# test optimizer if 'ro_t1' in params
|
# test optimizer if 'ro_t1' in params
|
||||||
|
|
||||||
def test_format_results():
|
def test_format_results(init_hyperopt):
|
||||||
"""
|
"""
|
||||||
Test Hyperopt.format_results()
|
Test Hyperopt.format_results()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Test with BTC as stake_currency
|
||||||
trades = [
|
trades = [
|
||||||
('ETH/BTC', 2, 2, 123),
|
('ETH/BTC', 2, 2, 123),
|
||||||
('LTC/BTC', 1, 1, 123),
|
('LTC/BTC', 1, 1, 123),
|
||||||
@ -400,8 +402,21 @@ def test_format_results():
|
|||||||
]
|
]
|
||||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
||||||
df = pd.DataFrame.from_records(trades, columns=labels)
|
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||||
x = Hyperopt.format_results(df)
|
|
||||||
assert x.find(' 66.67%')
|
result = _HYPEROPT.format_results(df)
|
||||||
|
assert result.find(' 66.67%')
|
||||||
|
assert result.find('Total profit 1.00000000 BTC')
|
||||||
|
assert result.find('2.0000Σ %')
|
||||||
|
|
||||||
|
# Test with EUR as stake_currency
|
||||||
|
trades = [
|
||||||
|
('ETH/EUR', 2, 2, 123),
|
||||||
|
('LTC/EUR', 1, 1, 123),
|
||||||
|
('XPR/EUR', -1, -2, -246)
|
||||||
|
]
|
||||||
|
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||||
|
result = _HYPEROPT.format_results(df)
|
||||||
|
assert result.find('Total profit 1.00000000 EUR')
|
||||||
|
|
||||||
|
|
||||||
def test_signal_handler(mocker, init_hyperopt):
|
def test_signal_handler(mocker, init_hyperopt):
|
||||||
|
@ -11,6 +11,7 @@ from freqtrade.misc import file_dump_json
|
|||||||
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs, \
|
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs, \
|
||||||
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist, \
|
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist, \
|
||||||
load_cached_data_for_updating
|
load_cached_data_for_updating
|
||||||
|
from freqtrade.arguments import TimeRange
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
# Change this if modifying UNITTEST/BTC testdatafile
|
# Change this if modifying UNITTEST/BTC testdatafile
|
||||||
@ -99,7 +100,21 @@ def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog) -> None:
|
|||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
||||||
|
|
||||||
_backup_file(file)
|
_backup_file(file)
|
||||||
optimize.load_data(None, ticker_interval='1m', pairs=['MEME/BTC'])
|
# do not download a new pair if refresh_pairs isn't set
|
||||||
|
optimize.load_data(None,
|
||||||
|
ticker_interval='1m',
|
||||||
|
refresh_pairs=False,
|
||||||
|
pairs=['MEME/BTC'])
|
||||||
|
assert os.path.isfile(file) is False
|
||||||
|
assert log_has('No data for pair: "MEME/BTC", Interval: 1m. '
|
||||||
|
'Use --refresh-pairs-cached to download the data',
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
# download a new pair if refresh_pairs is set
|
||||||
|
optimize.load_data(None,
|
||||||
|
ticker_interval='1m',
|
||||||
|
refresh_pairs=True,
|
||||||
|
pairs=['MEME/BTC'])
|
||||||
assert os.path.isfile(file) is True
|
assert os.path.isfile(file) is True
|
||||||
assert log_has('Download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
|
assert log_has('Download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
|
||||||
_clean_test_file(file)
|
_clean_test_file(file)
|
||||||
@ -162,7 +177,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
|||||||
|
|
||||||
# timeframe starts earlier than the cached data
|
# timeframe starts earlier than the cached data
|
||||||
# should fully update data
|
# should fully update data
|
||||||
timerange = (('date', None), test_data[0][0] / 1000 - 1, None)
|
timerange = TimeRange('date', None, test_data[0][0] / 1000 - 1, 0)
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||||
'1m',
|
'1m',
|
||||||
timerange)
|
timerange)
|
||||||
@ -173,13 +188,13 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
|||||||
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 120
|
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 120
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||||
'1m',
|
'1m',
|
||||||
((None, 'line'), None, -num_lines))
|
TimeRange(None, 'line', 0, -num_lines))
|
||||||
assert data == []
|
assert data == []
|
||||||
assert start_ts < test_data[0][0] - 1
|
assert start_ts < test_data[0][0] - 1
|
||||||
|
|
||||||
# timeframe starts in the center of the cached data
|
# timeframe starts in the center of the cached data
|
||||||
# should return the chached data w/o the last item
|
# should return the chached data w/o the last item
|
||||||
timerange = (('date', None), test_data[0][0] / 1000 + 1, None)
|
timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0)
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||||
'1m',
|
'1m',
|
||||||
timerange)
|
timerange)
|
||||||
@ -188,7 +203,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
|||||||
|
|
||||||
# same with 'line' timeframe
|
# same with 'line' timeframe
|
||||||
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 30
|
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 30
|
||||||
timerange = ((None, 'line'), None, -num_lines)
|
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||||
'1m',
|
'1m',
|
||||||
timerange)
|
timerange)
|
||||||
@ -197,7 +212,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
|||||||
|
|
||||||
# timeframe starts after the chached data
|
# timeframe starts after the chached data
|
||||||
# should return the chached data w/o the last item
|
# should return the chached data w/o the last item
|
||||||
timerange = (('date', None), test_data[-1][0] / 1000 + 1, None)
|
timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 1, 0)
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||||
'1m',
|
'1m',
|
||||||
timerange)
|
timerange)
|
||||||
@ -206,7 +221,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
|||||||
|
|
||||||
# same with 'line' timeframe
|
# same with 'line' timeframe
|
||||||
num_lines = 30
|
num_lines = 30
|
||||||
timerange = ((None, 'line'), None, -num_lines)
|
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||||
'1m',
|
'1m',
|
||||||
timerange)
|
timerange)
|
||||||
@ -216,7 +231,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
|||||||
# no timeframe is set
|
# no timeframe is set
|
||||||
# should return the chached data w/o the last item
|
# should return the chached data w/o the last item
|
||||||
num_lines = 30
|
num_lines = 30
|
||||||
timerange = ((None, 'line'), None, -num_lines)
|
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||||
'1m',
|
'1m',
|
||||||
timerange)
|
timerange)
|
||||||
@ -225,7 +240,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
|||||||
|
|
||||||
# no datafile exist
|
# no datafile exist
|
||||||
# should return timestamp start time
|
# should return timestamp start time
|
||||||
timerange = (('date', None), now_ts - 10000, None)
|
timerange = TimeRange('date', None, now_ts - 10000, 0)
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
|
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
|
||||||
'1m',
|
'1m',
|
||||||
timerange)
|
timerange)
|
||||||
@ -234,7 +249,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
|||||||
|
|
||||||
# same with 'line' timeframe
|
# same with 'line' timeframe
|
||||||
num_lines = 30
|
num_lines = 30
|
||||||
timerange = ((None, 'line'), None, -num_lines)
|
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
|
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
|
||||||
'1m',
|
'1m',
|
||||||
timerange)
|
timerange)
|
||||||
@ -329,7 +344,7 @@ def test_trim_tickerlist() -> None:
|
|||||||
|
|
||||||
# Test the pattern ^(-\d+)$
|
# Test the pattern ^(-\d+)$
|
||||||
# This pattern uses the latest N elements
|
# This pattern uses the latest N elements
|
||||||
timerange = ((None, 'line'), None, -5)
|
timerange = TimeRange(None, 'line', 0, -5)
|
||||||
ticker = trim_tickerlist(ticker_list, timerange)
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
ticker_len = len(ticker)
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
@ -339,7 +354,7 @@ def test_trim_tickerlist() -> None:
|
|||||||
|
|
||||||
# Test the pattern ^(\d+)-$
|
# Test the pattern ^(\d+)-$
|
||||||
# This pattern keep X element from the end
|
# This pattern keep X element from the end
|
||||||
timerange = (('line', None), 5, None)
|
timerange = TimeRange('line', None, 5, 0)
|
||||||
ticker = trim_tickerlist(ticker_list, timerange)
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
ticker_len = len(ticker)
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
@ -349,7 +364,7 @@ def test_trim_tickerlist() -> None:
|
|||||||
|
|
||||||
# Test the pattern ^(\d+)-(\d+)$
|
# Test the pattern ^(\d+)-(\d+)$
|
||||||
# This pattern extract a window
|
# This pattern extract a window
|
||||||
timerange = (('index', 'index'), 5, 10)
|
timerange = TimeRange('index', 'index', 5, 10)
|
||||||
ticker = trim_tickerlist(ticker_list, timerange)
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
ticker_len = len(ticker)
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
@ -360,7 +375,7 @@ def test_trim_tickerlist() -> None:
|
|||||||
|
|
||||||
# Test the pattern ^(\d{8})-(\d{8})$
|
# Test the pattern ^(\d{8})-(\d{8})$
|
||||||
# This pattern extract a window between the dates
|
# This pattern extract a window between the dates
|
||||||
timerange = (('date', 'date'), ticker_list[5][0] / 1000, ticker_list[10][0] / 1000 - 1)
|
timerange = TimeRange('date', 'date', ticker_list[5][0] / 1000, ticker_list[10][0] / 1000 - 1)
|
||||||
ticker = trim_tickerlist(ticker_list, timerange)
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
ticker_len = len(ticker)
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
@ -371,7 +386,7 @@ def test_trim_tickerlist() -> None:
|
|||||||
|
|
||||||
# Test the pattern ^-(\d{8})$
|
# Test the pattern ^-(\d{8})$
|
||||||
# This pattern extracts elements from the start to the date
|
# This pattern extracts elements from the start to the date
|
||||||
timerange = ((None, 'date'), None, ticker_list[10][0] / 1000 - 1)
|
timerange = TimeRange(None, 'date', 0, ticker_list[10][0] / 1000 - 1)
|
||||||
ticker = trim_tickerlist(ticker_list, timerange)
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
ticker_len = len(ticker)
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
@ -381,7 +396,7 @@ def test_trim_tickerlist() -> None:
|
|||||||
|
|
||||||
# Test the pattern ^(\d{8})-$
|
# Test the pattern ^(\d{8})-$
|
||||||
# This pattern extracts elements from the date to now
|
# This pattern extracts elements from the date to now
|
||||||
timerange = (('date', None), ticker_list[10][0] / 1000 - 1, None)
|
timerange = TimeRange('date', None, ticker_list[10][0] / 1000 - 1, None)
|
||||||
ticker = trim_tickerlist(ticker_list, timerange)
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
ticker_len = len(ticker)
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
@ -391,7 +406,7 @@ def test_trim_tickerlist() -> None:
|
|||||||
|
|
||||||
# Test a wrong pattern
|
# Test a wrong pattern
|
||||||
# This pattern must return the list unchanged
|
# This pattern must return the list unchanged
|
||||||
timerange = ((None, None), None, 5)
|
timerange = TimeRange(None, None, None, 5)
|
||||||
ticker = trim_tickerlist(ticker_list, timerange)
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
ticker_len = len(ticker)
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
|
@ -7,8 +7,6 @@ Unit test file for rpc/rpc.py
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
|
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc.rpc import RPC
|
from freqtrade.rpc.rpc import RPC
|
||||||
@ -39,7 +37,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
get_fee=fee
|
get_fee=fee
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
@ -88,7 +86,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
|||||||
get_fee=fee
|
get_fee=fee
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
@ -123,7 +121,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
|||||||
get_fee=fee
|
get_fee=fee
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
stake_currency = default_conf['stake_currency']
|
stake_currency = default_conf['stake_currency']
|
||||||
fiat_display_currency = default_conf['fiat_display_currency']
|
fiat_display_currency = default_conf['fiat_display_currency']
|
||||||
|
|
||||||
@ -180,7 +178,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
|||||||
get_fee=fee
|
get_fee=fee
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
stake_currency = default_conf['stake_currency']
|
stake_currency = default_conf['stake_currency']
|
||||||
fiat_display_currency = default_conf['fiat_display_currency']
|
fiat_display_currency = default_conf['fiat_display_currency']
|
||||||
|
|
||||||
@ -206,15 +204,30 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
|||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
|
|
||||||
|
freqtradebot.create_trade()
|
||||||
|
trade = Trade.query.first()
|
||||||
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
|
trade.update(limit_buy_order)
|
||||||
|
|
||||||
|
# Update the ticker with a market going up
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker_sell_up
|
||||||
|
)
|
||||||
|
trade.update(limit_sell_order)
|
||||||
|
trade.close_date = datetime.utcnow()
|
||||||
|
trade.is_open = False
|
||||||
|
|
||||||
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
|
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
assert not error
|
assert not error
|
||||||
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
|
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
|
||||||
assert prec_satoshi(stats['profit_closed_percent'], 6.2)
|
assert prec_satoshi(stats['profit_closed_percent'], 6.2)
|
||||||
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
|
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
|
||||||
assert prec_satoshi(stats['profit_all_coin'], 6.217e-05)
|
assert prec_satoshi(stats['profit_all_coin'], 5.632e-05)
|
||||||
assert prec_satoshi(stats['profit_all_percent'], 6.2)
|
assert prec_satoshi(stats['profit_all_percent'], 2.81)
|
||||||
assert prec_satoshi(stats['profit_all_fiat'], 0.93255)
|
assert prec_satoshi(stats['profit_all_fiat'], 0.8448)
|
||||||
assert stats['trade_count'] == 1
|
assert stats['trade_count'] == 2
|
||||||
assert stats['first_trade_date'] == 'just now'
|
assert stats['first_trade_date'] == 'just now'
|
||||||
assert stats['latest_trade_date'] == 'just now'
|
assert stats['latest_trade_date'] == 'just now'
|
||||||
assert stats['avg_duration'] == '0:00:00'
|
assert stats['avg_duration'] == '0:00:00'
|
||||||
@ -243,7 +256,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
|
|||||||
get_fee=fee
|
get_fee=fee
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
stake_currency = default_conf['stake_currency']
|
stake_currency = default_conf['stake_currency']
|
||||||
fiat_display_currency = default_conf['fiat_display_currency']
|
fiat_display_currency = default_conf['fiat_display_currency']
|
||||||
|
|
||||||
@ -314,7 +327,7 @@ def test_rpc_balance_handle(default_conf, mocker):
|
|||||||
get_balances=MagicMock(return_value=mock_balance)
|
get_balances=MagicMock(return_value=mock_balance)
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
(error, res) = rpc.rpc_balance(default_conf['fiat_display_currency'])
|
(error, res) = rpc.rpc_balance(default_conf['fiat_display_currency'])
|
||||||
@ -344,7 +357,7 @@ def test_rpc_start(mocker, default_conf) -> None:
|
|||||||
get_ticker=MagicMock()
|
get_ticker=MagicMock()
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
|
|
||||||
@ -372,7 +385,7 @@ def test_rpc_stop(mocker, default_conf) -> None:
|
|||||||
get_ticker=MagicMock()
|
get_ticker=MagicMock()
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
|
|
||||||
@ -411,7 +424,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
|
|||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
@ -449,20 +462,44 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
|
|||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
assert cancel_order_mock.call_count == 0
|
assert cancel_order_mock.call_count == 0
|
||||||
# make an limit-buy open trade
|
# make an limit-buy open trade
|
||||||
|
trade = Trade.query.filter(Trade.id == '1').first()
|
||||||
|
filled_amount = trade.amount / 2
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.freqtradebot.exchange.get_order',
|
'freqtrade.freqtradebot.exchange.get_order',
|
||||||
return_value={
|
return_value={
|
||||||
'status': 'open',
|
'status': 'open',
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'side': 'buy'
|
'side': 'buy',
|
||||||
|
'filled': filled_amount
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
# check that the trade is called, which is done
|
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
|
||||||
# by ensuring exchange.cancel_order is called
|
# and trade amount is updated
|
||||||
(error, res) = rpc.rpc_forcesell('1')
|
(error, res) = rpc.rpc_forcesell('1')
|
||||||
assert not error
|
assert not error
|
||||||
assert res == ''
|
assert res == ''
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
|
assert trade.amount == filled_amount
|
||||||
|
|
||||||
|
freqtradebot.create_trade()
|
||||||
|
trade = Trade.query.filter(Trade.id == '2').first()
|
||||||
|
amount = trade.amount
|
||||||
|
# make an limit-buy open trade, if there is no 'filled', don't sell it
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.freqtradebot.exchange.get_order',
|
||||||
|
return_value={
|
||||||
|
'status': 'open',
|
||||||
|
'type': 'limit',
|
||||||
|
'side': 'buy',
|
||||||
|
'filled': None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
|
||||||
|
(error, res) = rpc.rpc_forcesell('2')
|
||||||
|
assert not error
|
||||||
|
assert res == ''
|
||||||
|
assert cancel_order_mock.call_count == 2
|
||||||
|
assert trade.amount == amount
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
# make an limit-sell open trade
|
# make an limit-sell open trade
|
||||||
@ -474,11 +511,11 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'side': 'sell'
|
'side': 'sell'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
(error, res) = rpc.rpc_forcesell('2')
|
(error, res) = rpc.rpc_forcesell('3')
|
||||||
assert not error
|
assert not error
|
||||||
assert res == ''
|
assert res == ''
|
||||||
# status quo, no exchange calls
|
# status quo, no exchange calls
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||||
@ -497,7 +534,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
|||||||
get_fee=fee
|
get_fee=fee
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -536,7 +573,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
|
|||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
(error, trades) = rpc.rpc_count()
|
(error, trades) = rpc.rpc_count()
|
||||||
|
@ -11,7 +11,6 @@ from datetime import datetime
|
|||||||
from random import randint
|
from random import randint
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from telegram import Update, Message, Chat
|
from telegram import Update, Message, Chat
|
||||||
from telegram.error import NetworkError
|
from telegram.error import NetworkError
|
||||||
|
|
||||||
@ -156,7 +155,7 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['telegram']['enabled'] = False
|
conf['telegram']['enabled'] = False
|
||||||
dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://')))
|
dummy = DummyCls(FreqtradeBot(conf))
|
||||||
dummy.dummy_handler(bot=MagicMock(), update=update)
|
dummy.dummy_handler(bot=MagicMock(), update=update)
|
||||||
assert dummy.state['called'] is True
|
assert dummy.state['called'] is True
|
||||||
assert log_has(
|
assert log_has(
|
||||||
@ -187,7 +186,7 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['telegram']['enabled'] = False
|
conf['telegram']['enabled'] = False
|
||||||
dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://')))
|
dummy = DummyCls(FreqtradeBot(conf))
|
||||||
dummy.dummy_handler(bot=MagicMock(), update=update)
|
dummy.dummy_handler(bot=MagicMock(), update=update)
|
||||||
assert dummy.state['called'] is False
|
assert dummy.state['called'] is False
|
||||||
assert not log_has(
|
assert not log_has(
|
||||||
@ -217,7 +216,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['telegram']['enabled'] = False
|
conf['telegram']['enabled'] = False
|
||||||
dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://')))
|
dummy = DummyCls(FreqtradeBot(conf))
|
||||||
dummy.dummy_exception(bot=MagicMock(), update=update)
|
dummy.dummy_exception(bot=MagicMock(), update=update)
|
||||||
assert dummy.state['called'] is False
|
assert dummy.state['called'] is False
|
||||||
assert not log_has(
|
assert not log_has(
|
||||||
@ -263,7 +262,7 @@ def test_status(default_conf, update, mocker, fee, ticker) -> None:
|
|||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -301,7 +300,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
|
|||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
@ -348,7 +347,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
|
|||||||
|
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['stake_amount'] = 15.0
|
conf['stake_amount'] = 15.0
|
||||||
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
@ -402,7 +401,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
|||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -470,7 +469,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
|||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Try invalid data
|
# Try invalid data
|
||||||
@ -511,7 +510,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
|||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._profit(bot=MagicMock(), update=update)
|
telegram._profit(bot=MagicMock(), update=update)
|
||||||
@ -608,7 +607,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
|||||||
send_msg=msg_mock
|
send_msg=msg_mock
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._balance(bot=MagicMock(), update=update)
|
telegram._balance(bot=MagicMock(), update=update)
|
||||||
@ -638,7 +637,7 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None:
|
|||||||
send_msg=msg_mock
|
send_msg=msg_mock
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._balance(bot=MagicMock(), update=update)
|
telegram._balance(bot=MagicMock(), update=update)
|
||||||
@ -661,7 +660,7 @@ def test_start_handle(default_conf, update, mocker) -> None:
|
|||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
@ -685,7 +684,7 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None:
|
|||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
@ -710,7 +709,7 @@ def test_stop_handle(default_conf, update, mocker) -> None:
|
|||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
@ -735,7 +734,7 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
|
|||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
@ -762,7 +761,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, moc
|
|||||||
get_fee=fee
|
get_fee=fee
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -802,7 +801,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, ticker_sell_do
|
|||||||
get_fee=fee
|
get_fee=fee
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -847,7 +846,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
|
|||||||
get_fee=fee
|
get_fee=fee
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -880,7 +879,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
|||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Trader is not running
|
# Trader is not running
|
||||||
@ -927,7 +926,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
|
|||||||
get_fee=fee
|
get_fee=fee
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -962,7 +961,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
|
|||||||
send_msg=msg_mock
|
send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Trader is not running
|
# Trader is not running
|
||||||
@ -991,7 +990,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
|
|||||||
buy=MagicMock(return_value={'id': 'mocked_order_id'})
|
buy=MagicMock(return_value={'id': 'mocked_order_id'})
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
|
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
@ -1027,7 +1026,7 @@ def test_help_handle(default_conf, update, mocker) -> None:
|
|||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
send_msg=msg_mock
|
||||||
)
|
)
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._help(bot=MagicMock(), update=update)
|
telegram._help(bot=MagicMock(), update=update)
|
||||||
@ -1047,7 +1046,7 @@ def test_version_handle(default_conf, update, mocker) -> None:
|
|||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
send_msg=msg_mock
|
||||||
)
|
)
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._version(bot=MagicMock(), update=update)
|
telegram._version(bot=MagicMock(), update=update)
|
||||||
@ -1064,7 +1063,7 @@ def test_send_msg(default_conf, mocker) -> None:
|
|||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
bot = MagicMock()
|
bot = MagicMock()
|
||||||
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._config['telegram']['enabled'] = False
|
telegram._config['telegram']['enabled'] = False
|
||||||
@ -1087,7 +1086,7 @@ def test_send_msg_network_error(default_conf, mocker, caplog) -> None:
|
|||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
bot = MagicMock()
|
bot = MagicMock()
|
||||||
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
|
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
|
||||||
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._config['telegram']['enabled'] = True
|
telegram._config['telegram']['enabled'] = True
|
||||||
|
@ -13,6 +13,7 @@ from pandas import DataFrame
|
|||||||
|
|
||||||
from freqtrade.analyze import Analyze, SignalType
|
from freqtrade.analyze import Analyze, SignalType
|
||||||
from freqtrade.optimize.__init__ import load_tickerdata_file
|
from freqtrade.optimize.__init__ import load_tickerdata_file
|
||||||
|
from freqtrade.arguments import TimeRange
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
# Avoid to reinit the same object again and again
|
# Avoid to reinit the same object again and again
|
||||||
@ -45,7 +46,7 @@ def test_analyze_object() -> None:
|
|||||||
|
|
||||||
def test_dataframe_correct_length(result):
|
def test_dataframe_correct_length(result):
|
||||||
dataframe = Analyze.parse_ticker_dataframe(result)
|
dataframe = Analyze.parse_ticker_dataframe(result)
|
||||||
assert len(result.index) == len(dataframe.index)
|
assert len(result.index) - 1 == len(dataframe.index) # last partial candle removed
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_correct_columns(result):
|
def test_dataframe_correct_columns(result):
|
||||||
@ -183,8 +184,8 @@ def test_tickerdata_to_dataframe(default_conf) -> None:
|
|||||||
"""
|
"""
|
||||||
analyze = Analyze(default_conf)
|
analyze = Analyze(default_conf)
|
||||||
|
|
||||||
timerange = ((None, 'line'), None, -100)
|
timerange = TimeRange(None, 'line', 0, -100)
|
||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
tickerlist = {'UNITTEST/BTC': tick}
|
tickerlist = {'UNITTEST/BTC': tick}
|
||||||
data = analyze.tickerdata_to_dataframe(tickerlist)
|
data = analyze.tickerdata_to_dataframe(tickerlist)
|
||||||
assert len(data['UNITTEST/BTC']) == 100
|
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed
|
||||||
|
@ -9,7 +9,7 @@ import logging
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments, TimeRange
|
||||||
|
|
||||||
|
|
||||||
def test_arguments_object() -> None:
|
def test_arguments_object() -> None:
|
||||||
@ -46,6 +46,11 @@ def test_parse_args_config() -> None:
|
|||||||
assert args.config == '/dev/null'
|
assert args.config == '/dev/null'
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_db_url() -> None:
|
||||||
|
args = Arguments(['--db-url', 'sqlite:///test.sqlite'], '').get_parsed_arg()
|
||||||
|
assert args.db_url == 'sqlite:///test.sqlite'
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_verbose() -> None:
|
def test_parse_args_verbose() -> None:
|
||||||
args = Arguments(['-v'], '').get_parsed_arg()
|
args = Arguments(['-v'], '').get_parsed_arg()
|
||||||
assert args.loglevel == logging.DEBUG
|
assert args.loglevel == logging.DEBUG
|
||||||
@ -107,14 +112,24 @@ def test_parse_args_dynamic_whitelist_invalid_values() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_parse_timerange_incorrect() -> None:
|
def test_parse_timerange_incorrect() -> None:
|
||||||
assert ((None, 'line'), None, -200) == Arguments.parse_timerange('-200')
|
assert TimeRange(None, 'line', 0, -200) == Arguments.parse_timerange('-200')
|
||||||
assert (('line', None), 200, None) == Arguments.parse_timerange('200-')
|
assert TimeRange('line', None, 200, 0) == Arguments.parse_timerange('200-')
|
||||||
assert (('index', 'index'), 200, 500) == Arguments.parse_timerange('200-500')
|
assert TimeRange('index', 'index', 200, 500) == Arguments.parse_timerange('200-500')
|
||||||
|
|
||||||
assert (('date', None), 1274486400, None) == Arguments.parse_timerange('20100522-')
|
assert TimeRange('date', None, 1274486400, 0) == Arguments.parse_timerange('20100522-')
|
||||||
assert ((None, 'date'), None, 1274486400) == Arguments.parse_timerange('-20100522')
|
assert TimeRange(None, 'date', 0, 1274486400) == Arguments.parse_timerange('-20100522')
|
||||||
timerange = Arguments.parse_timerange('20100522-20150730')
|
timerange = Arguments.parse_timerange('20100522-20150730')
|
||||||
assert timerange == (('date', 'date'), 1274486400, 1438214400)
|
assert timerange == TimeRange('date', 'date', 1274486400, 1438214400)
|
||||||
|
|
||||||
|
# Added test for unix timestamp - BTC genesis date
|
||||||
|
assert TimeRange('date', None, 1231006505, 0) == Arguments.parse_timerange('1231006505-')
|
||||||
|
assert TimeRange(None, 'date', 0, 1233360000) == Arguments.parse_timerange('-1233360000')
|
||||||
|
timerange = Arguments.parse_timerange('1231006505-1233360000')
|
||||||
|
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
|
||||||
|
|
||||||
|
# TODO: Find solution for the following case (passing timestamp in ms)
|
||||||
|
timerange = Arguments.parse_timerange('1231006505000-1233360000000')
|
||||||
|
assert TimeRange('date', 'date', 1231006505, 1233360000) != timerange
|
||||||
|
|
||||||
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
|
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
|
||||||
Arguments.parse_timerange('-')
|
Arguments.parse_timerange('-')
|
||||||
@ -159,3 +174,19 @@ def test_parse_args_hyperopt_custom() -> None:
|
|||||||
assert call_args.subparser == 'hyperopt'
|
assert call_args.subparser == 'hyperopt'
|
||||||
assert call_args.spaces == ['buy']
|
assert call_args.spaces == ['buy']
|
||||||
assert call_args.func is not None
|
assert call_args.func is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_testdata_dl_options() -> None:
|
||||||
|
args = [
|
||||||
|
'--pairs-file', 'file_with_pairs',
|
||||||
|
'--export', 'export/folder',
|
||||||
|
'--days', '30',
|
||||||
|
'--exchange', 'binance'
|
||||||
|
]
|
||||||
|
arguments = Arguments(args, '')
|
||||||
|
arguments.testdata_dl_options()
|
||||||
|
args = arguments.parse_args()
|
||||||
|
assert args.pairs_file == 'file_with_pairs'
|
||||||
|
assert args.export == 'export/folder'
|
||||||
|
assert args.days == 30
|
||||||
|
assert args.exchange == 'binance'
|
||||||
|
@ -6,6 +6,7 @@ Unit test file for configuration.py
|
|||||||
import json
|
import json
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from jsonschema import ValidationError
|
from jsonschema import ValidationError
|
||||||
@ -37,7 +38,7 @@ def test_load_config_invalid_pair(default_conf) -> None:
|
|||||||
conf['exchange']['pair_whitelist'].append('ETH-BTC')
|
conf['exchange']['pair_whitelist'].append('ETH-BTC')
|
||||||
|
|
||||||
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
||||||
configuration = Configuration([])
|
configuration = Configuration(Namespace())
|
||||||
configuration._validate_config(conf)
|
configuration._validate_config(conf)
|
||||||
|
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ def test_load_config_missing_attributes(default_conf) -> None:
|
|||||||
conf.pop('exchange')
|
conf.pop('exchange')
|
||||||
|
|
||||||
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
|
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
|
||||||
configuration = Configuration([])
|
configuration = Configuration(Namespace())
|
||||||
configuration._validate_config(conf)
|
configuration._validate_config(conf)
|
||||||
|
|
||||||
|
|
||||||
@ -61,7 +62,7 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
|
|||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
configuration = Configuration([])
|
configuration = Configuration(Namespace())
|
||||||
validated_conf = configuration._load_config_file('somefile')
|
validated_conf = configuration._load_config_file('somefile')
|
||||||
assert file_mock.call_count == 1
|
assert file_mock.call_count == 1
|
||||||
assert validated_conf.items() >= default_conf.items()
|
assert validated_conf.items() >= default_conf.items()
|
||||||
@ -79,12 +80,12 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
|
|||||||
read_data=json.dumps(conf)
|
read_data=json.dumps(conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
Configuration([])._load_config_file('somefile')
|
Configuration(Namespace())._load_config_file('somefile')
|
||||||
assert file_mock.call_count == 1
|
assert file_mock.call_count == 1
|
||||||
assert log_has('Validating configuration ...', caplog.record_tuples)
|
assert log_has('Validating configuration ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_file_exception(mocker, caplog) -> None:
|
def test_load_config_file_exception(mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test Configuration._load_config_file() method
|
Test Configuration._load_config_file() method
|
||||||
"""
|
"""
|
||||||
@ -92,14 +93,10 @@ def test_load_config_file_exception(mocker, caplog) -> None:
|
|||||||
'freqtrade.configuration.open',
|
'freqtrade.configuration.open',
|
||||||
MagicMock(side_effect=FileNotFoundError('File not found'))
|
MagicMock(side_effect=FileNotFoundError('File not found'))
|
||||||
)
|
)
|
||||||
configuration = Configuration([])
|
configuration = Configuration(Namespace())
|
||||||
|
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(OperationalException, match=r'.*Config file "somefile" not found!*'):
|
||||||
configuration._load_config_file('somefile')
|
configuration._load_config_file('somefile')
|
||||||
assert log_has(
|
|
||||||
'Config file "somefile" not found. Please create your config file',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_config(default_conf, mocker) -> None:
|
def test_load_config(default_conf, mocker) -> None:
|
||||||
@ -117,7 +114,6 @@ def test_load_config(default_conf, mocker) -> None:
|
|||||||
assert validated_conf.get('strategy') == 'DefaultStrategy'
|
assert validated_conf.get('strategy') == 'DefaultStrategy'
|
||||||
assert validated_conf.get('strategy_path') is None
|
assert validated_conf.get('strategy_path') is None
|
||||||
assert 'dynamic_whitelist' not in validated_conf
|
assert 'dynamic_whitelist' not in validated_conf
|
||||||
assert 'dry_run_db' not in validated_conf
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_with_params(default_conf, mocker) -> None:
|
def test_load_config_with_params(default_conf, mocker) -> None:
|
||||||
@ -128,13 +124,13 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
|||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
args = [
|
arglist = [
|
||||||
'--dynamic-whitelist', '10',
|
'--dynamic-whitelist', '10',
|
||||||
'--strategy', 'TestStrategy',
|
'--strategy', 'TestStrategy',
|
||||||
'--strategy-path', '/some/path',
|
'--strategy-path', '/some/path',
|
||||||
'--dry-run-db',
|
'--db-url', 'sqlite:///someurl',
|
||||||
]
|
]
|
||||||
args = Arguments(args, '').get_parsed_arg()
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
validated_conf = configuration.load_config()
|
validated_conf = configuration.load_config()
|
||||||
@ -142,7 +138,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
|||||||
assert validated_conf.get('dynamic_whitelist') == 10
|
assert validated_conf.get('dynamic_whitelist') == 10
|
||||||
assert validated_conf.get('strategy') == 'TestStrategy'
|
assert validated_conf.get('strategy') == 'TestStrategy'
|
||||||
assert validated_conf.get('strategy_path') == '/some/path'
|
assert validated_conf.get('strategy_path') == '/some/path'
|
||||||
assert validated_conf.get('dry_run_db') is True
|
assert validated_conf.get('db_url') == 'sqlite:///someurl'
|
||||||
|
|
||||||
|
|
||||||
def test_load_custom_strategy(default_conf, mocker) -> None:
|
def test_load_custom_strategy(default_conf, mocker) -> None:
|
||||||
@ -174,12 +170,12 @@ def test_show_info(default_conf, mocker, caplog) -> None:
|
|||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
args = [
|
arglist = [
|
||||||
'--dynamic-whitelist', '10',
|
'--dynamic-whitelist', '10',
|
||||||
'--strategy', 'TestStrategy',
|
'--strategy', 'TestStrategy',
|
||||||
'--dry-run-db'
|
'--db-url', 'sqlite:///tmp/testdb',
|
||||||
]
|
]
|
||||||
args = Arguments(args, '').get_parsed_arg()
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
configuration.get_config()
|
configuration.get_config()
|
||||||
@ -191,23 +187,8 @@ def test_show_info(default_conf, mocker, caplog) -> None:
|
|||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
|
|
||||||
assert log_has(
|
assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples)
|
||||||
'Parameter --dry-run-db detected ...',
|
assert log_has('Dry run is enabled', caplog.record_tuples)
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
assert log_has(
|
|
||||||
'Dry_run will use the DB file: "tradesv3.dry_run.sqlite"',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test the Dry run condition
|
|
||||||
configuration.config.update({'dry_run': False})
|
|
||||||
configuration._load_common_config(configuration.config)
|
|
||||||
assert log_has(
|
|
||||||
'Dry run is disabled. (--dry_run_db ignored)',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||||
@ -218,13 +199,13 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
|||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
args = [
|
arglist = [
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--strategy', 'DefaultStrategy',
|
'--strategy', 'DefaultStrategy',
|
||||||
'backtesting'
|
'backtesting'
|
||||||
]
|
]
|
||||||
|
|
||||||
args = Arguments(args, '').get_parsed_arg()
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
config = configuration.get_config()
|
config = configuration.get_config()
|
||||||
@ -235,7 +216,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
|||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
'Using data folder: {} ...'.format(config['datadir']),
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
@ -262,7 +243,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
args = [
|
arglist = [
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--strategy', 'DefaultStrategy',
|
'--strategy', 'DefaultStrategy',
|
||||||
'--datadir', '/foo/bar',
|
'--datadir', '/foo/bar',
|
||||||
@ -275,7 +256,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
'--export', '/bar/foo'
|
'--export', '/bar/foo'
|
||||||
]
|
]
|
||||||
|
|
||||||
args = Arguments(args, '').get_parsed_arg()
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
config = configuration.get_config()
|
config = configuration.get_config()
|
||||||
@ -286,7 +267,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
'Using data folder: {} ...'.format(config['datadir']),
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
@ -326,14 +307,14 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
|||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
args = [
|
arglist = [
|
||||||
'hyperopt',
|
'hyperopt',
|
||||||
'--epochs', '10',
|
'--epochs', '10',
|
||||||
'--use-mongodb',
|
'--use-mongodb',
|
||||||
'--spaces', 'all',
|
'--spaces', 'all',
|
||||||
]
|
]
|
||||||
|
|
||||||
args = Arguments(args, '').get_parsed_arg()
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
config = configuration.get_config()
|
config = configuration.get_config()
|
||||||
@ -357,7 +338,7 @@ def test_check_exchange(default_conf) -> None:
|
|||||||
Test the configuration validator with a missing attribute
|
Test the configuration validator with a missing attribute
|
||||||
"""
|
"""
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
configuration = Configuration([])
|
configuration = Configuration(Namespace())
|
||||||
|
|
||||||
# Test a valid exchange
|
# Test a valid exchange
|
||||||
conf.get('exchange').update({'name': 'BITTREX'})
|
conf.get('exchange').update({'name': 'BITTREX'})
|
||||||
|
@ -6,8 +6,10 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
|
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
|
||||||
from freqtrade.tests.conftest import patch_coinmarketcap
|
from freqtrade.tests.conftest import log_has, patch_coinmarketcap
|
||||||
|
|
||||||
|
|
||||||
def test_pair_convertion_object():
|
def test_pair_convertion_object():
|
||||||
@ -88,6 +90,13 @@ def test_fiat_convert_find_price(mocker):
|
|||||||
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 13000.2
|
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 13000.2
|
||||||
|
|
||||||
|
|
||||||
|
def test_fiat_convert_unsupported_crypto(mocker, caplog):
|
||||||
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[])
|
||||||
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0
|
||||||
|
assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_fiat_convert_get_price(mocker):
|
def test_fiat_convert_get_price(mocker):
|
||||||
api_mock = MagicMock(return_value={
|
api_mock = MagicMock(return_value={
|
||||||
'price_usd': 28000.0,
|
'price_usd': 28000.0,
|
||||||
@ -124,6 +133,20 @@ def test_fiat_convert_get_price(mocker):
|
|||||||
assert fiat_convert._pairs[0]._expiration is not expiration
|
assert fiat_convert._pairs[0]._expiration is not expiration
|
||||||
|
|
||||||
|
|
||||||
|
def test_fiat_convert_same_currencies(mocker):
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
|
||||||
|
assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='USD') == 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_fiat_convert_two_FIAT(mocker):
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
|
||||||
|
assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='EUR') == 0.0
|
||||||
|
|
||||||
|
|
||||||
def test_loadcryptomap(mocker):
|
def test_loadcryptomap(mocker):
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
|
||||||
@ -133,6 +156,22 @@ def test_loadcryptomap(mocker):
|
|||||||
assert fiat_convert._cryptomap["BTC"] == "1"
|
assert fiat_convert._cryptomap["BTC"] == "1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_fiat_init_network_exception(mocker):
|
||||||
|
# Because CryptoToFiatConverter is a Singleton we reset the listings
|
||||||
|
listmock = MagicMock(side_effect=RequestException)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.fiat_convert.Market',
|
||||||
|
listings=listmock,
|
||||||
|
)
|
||||||
|
# with pytest.raises(RequestEsxception):
|
||||||
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
fiat_convert._cryptomap = {}
|
||||||
|
fiat_convert._load_cryptomap()
|
||||||
|
|
||||||
|
length_cryptomap = len(fiat_convert._cryptomap)
|
||||||
|
assert length_cryptomap == 0
|
||||||
|
|
||||||
|
|
||||||
def test_fiat_convert_without_network():
|
def test_fiat_convert_without_network():
|
||||||
# Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap
|
# Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap
|
||||||
|
|
||||||
@ -144,3 +183,22 @@ def test_fiat_convert_without_network():
|
|||||||
assert fiat_convert._coinmarketcap is None
|
assert fiat_convert._coinmarketcap is None
|
||||||
assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0
|
assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0
|
||||||
CryptoToFiatConverter._coinmarketcap = cmc_temp
|
CryptoToFiatConverter._coinmarketcap = cmc_temp
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_amount(mocker):
|
||||||
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0)
|
||||||
|
|
||||||
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
result = fiat_convert.convert_amount(
|
||||||
|
crypto_amount=1.23,
|
||||||
|
crypto_symbol="BTC",
|
||||||
|
fiat_symbol="USD"
|
||||||
|
)
|
||||||
|
assert result == 15184.35
|
||||||
|
|
||||||
|
result = fiat_convert.convert_amount(
|
||||||
|
crypto_amount=1.23,
|
||||||
|
crypto_symbol="BTC",
|
||||||
|
fiat_symbol="BTC"
|
||||||
|
)
|
||||||
|
assert result == 1.23
|
||||||
|
@ -13,7 +13,6 @@ from unittest.mock import MagicMock
|
|||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
from sqlalchemy import create_engine
|
|
||||||
|
|
||||||
from freqtrade import DependencyException, OperationalException, TemporaryError
|
from freqtrade import DependencyException, OperationalException, TemporaryError
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
@ -36,7 +35,7 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
|||||||
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
|
||||||
return FreqtradeBot(config, create_engine('sqlite://'))
|
return FreqtradeBot(config)
|
||||||
|
|
||||||
|
|
||||||
def patch_get_signal(mocker, value=(True, False)) -> None:
|
def patch_get_signal(mocker, value=(True, False)) -> None:
|
||||||
@ -237,7 +236,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non
|
|||||||
|
|
||||||
# Save state of current whitelist
|
# Save state of current whitelist
|
||||||
whitelist = deepcopy(default_conf['exchange']['pair_whitelist'])
|
whitelist = deepcopy(default_conf['exchange']['pair_whitelist'])
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
@ -274,7 +273,7 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, fee,
|
|||||||
|
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['stake_amount'] = 0.0005
|
conf['stake_amount'] = 0.0005
|
||||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(conf)
|
||||||
|
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2]
|
rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2]
|
||||||
@ -296,7 +295,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee
|
|||||||
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5),
|
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
|
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
@ -320,7 +319,7 @@ def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, mocke
|
|||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
|
conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
|
||||||
conf['exchange']['pair_blacklist'] = ["ETH/BTC"]
|
conf['exchange']['pair_blacklist'] = ["ETH/BTC"]
|
||||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(conf)
|
||||||
|
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
@ -347,7 +346,7 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker,
|
|||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
|
conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
|
||||||
conf['exchange']['pair_blacklist'] = ["ETH/BTC"]
|
conf['exchange']['pair_blacklist'] = ["ETH/BTC"]
|
||||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(conf)
|
||||||
|
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
@ -375,7 +374,7 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None:
|
|||||||
|
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['stake_amount'] = 10
|
conf['stake_amount'] = 10
|
||||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(conf)
|
||||||
|
|
||||||
Trade.query = MagicMock()
|
Trade.query = MagicMock()
|
||||||
Trade.query.filter = MagicMock()
|
Trade.query.filter = MagicMock()
|
||||||
@ -399,7 +398,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order,
|
|||||||
get_order=MagicMock(return_value=limit_buy_order),
|
get_order=MagicMock(return_value=limit_buy_order),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
assert not trades
|
assert not trades
|
||||||
@ -440,7 +439,7 @@ def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> Non
|
|||||||
)
|
)
|
||||||
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
|
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
result = freqtrade._process()
|
result = freqtrade._process()
|
||||||
assert result is False
|
assert result is False
|
||||||
assert sleep_mock.has_calls()
|
assert sleep_mock.has_calls()
|
||||||
@ -460,7 +459,7 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) ->
|
|||||||
get_markets=markets,
|
get_markets=markets,
|
||||||
buy=MagicMock(side_effect=OperationalException)
|
buy=MagicMock(side_effect=OperationalException)
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
assert freqtrade.state == State.RUNNING
|
assert freqtrade.state == State.RUNNING
|
||||||
|
|
||||||
result = freqtrade._process()
|
result = freqtrade._process()
|
||||||
@ -486,7 +485,7 @@ def test_process_trade_handling(
|
|||||||
get_order=MagicMock(return_value=limit_buy_order),
|
get_order=MagicMock(return_value=limit_buy_order),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
assert not trades
|
assert not trades
|
||||||
@ -503,27 +502,27 @@ def test_balance_fully_ask_side(mocker) -> None:
|
|||||||
"""
|
"""
|
||||||
Test get_target_bid() method
|
Test get_target_bid() method
|
||||||
"""
|
"""
|
||||||
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'ask_last_balance': 0.0}})
|
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'use_book_order':False,'book_order_top':6,'ask_last_balance': 0.0}})
|
||||||
|
|
||||||
assert freqtrade.get_target_bid({'ask': 20, 'last': 10}) == 20
|
assert freqtrade.get_target_bid('ETH/BTC') >= 0.07
|
||||||
|
|
||||||
|
|
||||||
def test_balance_fully_last_side(mocker) -> None:
|
def test_balance_fully_last_side(mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test get_target_bid() method
|
Test get_target_bid() method
|
||||||
"""
|
"""
|
||||||
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'ask_last_balance': 1.0}})
|
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'use_book_order':False,'book_order_top':6,'ask_last_balance': 1.0}})
|
||||||
|
|
||||||
assert freqtrade.get_target_bid({'ask': 20, 'last': 10}) == 10
|
assert freqtrade.get_target_bid('ETH/BTC') >= 0.07
|
||||||
|
|
||||||
|
|
||||||
def test_balance_bigger_last_ask(mocker) -> None:
|
def test_balance_bigger_last_ask(mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test get_target_bid() method
|
Test get_target_bid() method
|
||||||
"""
|
"""
|
||||||
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'ask_last_balance': 1.0}})
|
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'use_book_order':False,'book_order_top':6,'ask_last_balance': 1.0}})
|
||||||
|
|
||||||
assert freqtrade.get_target_bid({'ask': 5, 'last': 10}) == 5
|
assert freqtrade.get_target_bid('ETH/BTC') >= 0.07
|
||||||
|
|
||||||
|
|
||||||
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
|
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
|
||||||
@ -570,8 +569,10 @@ def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplo
|
|||||||
trade.open_fee = 0.001
|
trade.open_fee = 0.001
|
||||||
assert not freqtrade.process_maybe_execute_sell(trade)
|
assert not freqtrade.process_maybe_execute_sell(trade)
|
||||||
# Test amount not modified by fee-logic
|
# Test amount not modified by fee-logic
|
||||||
assert not log_has('Applying fee to amount for Trade {} from 90.99181073 to 90.81'.format(
|
assert not log_has(
|
||||||
trade), caplog.record_tuples)
|
'Applying fee to amount for Trade {} from 90.99181073 to 90.81'.format(trade),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81)
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81)
|
||||||
# test amount modified by fee-logic
|
# test amount modified by fee-logic
|
||||||
@ -582,6 +583,38 @@ def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplo
|
|||||||
# Assert we call handle_trade() if trade is feasible for execution
|
# Assert we call handle_trade() if trade is feasible for execution
|
||||||
assert freqtrade.process_maybe_execute_sell(trade)
|
assert freqtrade.process_maybe_execute_sell(trade)
|
||||||
|
|
||||||
|
regexp = re.compile('Found open order for.*')
|
||||||
|
assert filter(regexp.match, caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_maybe_execute_sell_exception(mocker, default_conf,
|
||||||
|
limit_buy_order, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test the exceptions in process_maybe_execute_sell()
|
||||||
|
"""
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.exchange.get_order', return_value=limit_buy_order)
|
||||||
|
|
||||||
|
trade = MagicMock()
|
||||||
|
trade.open_order_id = '123'
|
||||||
|
trade.open_fee = 0.001
|
||||||
|
|
||||||
|
# Test raise of OperationalException exception
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
|
||||||
|
side_effect=OperationalException()
|
||||||
|
)
|
||||||
|
freqtrade.process_maybe_execute_sell(trade)
|
||||||
|
assert log_has('could not update trade amount: ', caplog.record_tuples)
|
||||||
|
|
||||||
|
# Test raise of DependencyException exception
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
|
||||||
|
side_effect=DependencyException()
|
||||||
|
)
|
||||||
|
freqtrade.process_maybe_execute_sell(trade)
|
||||||
|
assert log_has('Unable to sell trade: ', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mocker) -> None:
|
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
@ -603,7 +636,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mock
|
|||||||
)
|
)
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
@ -646,7 +679,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee,
|
|||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(conf)
|
||||||
|
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
@ -705,7 +738,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, fee, mocker, ca
|
|||||||
)
|
)
|
||||||
|
|
||||||
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=True)
|
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=True)
|
||||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(conf)
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
@ -742,7 +775,7 @@ def test_handle_trade_experimental(
|
|||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False)
|
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False)
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(conf)
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
@ -770,7 +803,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, fe
|
|||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
# Create trade and sell it
|
# Create trade and sell it
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
@ -801,7 +834,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fe
|
|||||||
cancel_order=cancel_order_mock,
|
cancel_order=cancel_order_mock,
|
||||||
get_fee=fee
|
get_fee=fee
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
trade_buy = Trade(
|
trade_buy = Trade(
|
||||||
pair='ETH/BTC',
|
pair='ETH/BTC',
|
||||||
@ -841,7 +874,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
|
|||||||
get_order=MagicMock(return_value=limit_sell_order_old),
|
get_order=MagicMock(return_value=limit_sell_order_old),
|
||||||
cancel_order=cancel_order_mock
|
cancel_order=cancel_order_mock
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
trade_sell = Trade(
|
trade_sell = Trade(
|
||||||
pair='ETH/BTC',
|
pair='ETH/BTC',
|
||||||
@ -881,7 +914,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
|
|||||||
get_order=MagicMock(return_value=limit_buy_order_old_partial),
|
get_order=MagicMock(return_value=limit_buy_order_old_partial),
|
||||||
cancel_order=cancel_order_mock
|
cancel_order=cancel_order_mock
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
trade_buy = Trade(
|
trade_buy = Trade(
|
||||||
pair='ETH/BTC',
|
pair='ETH/BTC',
|
||||||
@ -929,7 +962,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) -
|
|||||||
get_order=MagicMock(side_effect=requests.exceptions.RequestException('Oh snap')),
|
get_order=MagicMock(side_effect=requests.exceptions.RequestException('Oh snap')),
|
||||||
cancel_order=cancel_order_mock
|
cancel_order=cancel_order_mock
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
trade_buy = Trade(
|
trade_buy = Trade(
|
||||||
pair='ETH/BTC',
|
pair='ETH/BTC',
|
||||||
@ -968,7 +1001,7 @@ def test_handle_timedout_limit_buy(mocker, default_conf) -> None:
|
|||||||
cancel_order=cancel_order_mock
|
cancel_order=cancel_order_mock
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
Trade.session = MagicMock()
|
Trade.session = MagicMock()
|
||||||
trade = MagicMock()
|
trade = MagicMock()
|
||||||
@ -994,7 +1027,7 @@ def test_handle_timedout_limit_sell(mocker, default_conf) -> None:
|
|||||||
cancel_order=cancel_order_mock
|
cancel_order=cancel_order_mock
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
trade = MagicMock()
|
trade = MagicMock()
|
||||||
order = {'remaining': 1,
|
order = {'remaining': 1,
|
||||||
@ -1021,7 +1054,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
|
|||||||
get_fee=fee
|
get_fee=fee
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
@ -1062,7 +1095,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
|
|||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee
|
get_fee=fee
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
@ -1102,7 +1135,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
|
|||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee
|
get_fee=fee
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
@ -1143,7 +1176,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee,
|
|||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee
|
get_fee=fee
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
@ -1192,7 +1225,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, fee, mock
|
|||||||
'use_sell_signal': True,
|
'use_sell_signal': True,
|
||||||
'sell_profit_only': True,
|
'sell_profit_only': True,
|
||||||
}
|
}
|
||||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(conf)
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
@ -1225,7 +1258,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, fee, moc
|
|||||||
'use_sell_signal': True,
|
'use_sell_signal': True,
|
||||||
'sell_profit_only': False,
|
'sell_profit_only': False,
|
||||||
}
|
}
|
||||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(conf)
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
@ -1258,7 +1291,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker
|
|||||||
'use_sell_signal': True,
|
'use_sell_signal': True,
|
||||||
'sell_profit_only': True,
|
'sell_profit_only': True,
|
||||||
}
|
}
|
||||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(conf)
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
@ -1293,7 +1326,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke
|
|||||||
'sell_profit_only': False,
|
'sell_profit_only': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(conf)
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
@ -1321,7 +1354,7 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, ca
|
|||||||
open_rate=0.245441,
|
open_rate=0.245441,
|
||||||
open_order_id="123456"
|
open_order_id="123456"
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
# Amount is reduced by "fee"
|
# Amount is reduced by "fee"
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
|
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
|
||||||
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
||||||
@ -1348,7 +1381,7 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker):
|
|||||||
open_rate=0.245441,
|
open_rate=0.245441,
|
||||||
open_order_id="123456"
|
open_order_id="123456"
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
# Amount is reduced by "fee"
|
# Amount is reduced by "fee"
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
||||||
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
||||||
@ -1356,7 +1389,7 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker):
|
|||||||
caplog.record_tuples)
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, caplog, mocker):
|
def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mocker):
|
||||||
"""
|
"""
|
||||||
Test get_real_amount - fees in Stake currency
|
Test get_real_amount - fees in Stake currency
|
||||||
"""
|
"""
|
||||||
@ -1375,7 +1408,7 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, ca
|
|||||||
open_rate=0.245441,
|
open_rate=0.245441,
|
||||||
open_order_id="123456"
|
open_order_id="123456"
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
# Amount does not change
|
# Amount does not change
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
||||||
|
|
||||||
@ -1401,7 +1434,7 @@ def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mock
|
|||||||
open_rate=0.245441,
|
open_rate=0.245441,
|
||||||
open_order_id="123456"
|
open_order_id="123456"
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
# Amount does not change
|
# Amount does not change
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
||||||
|
|
||||||
@ -1424,7 +1457,7 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c
|
|||||||
open_rate=0.245441,
|
open_rate=0.245441,
|
||||||
open_order_id="123456"
|
open_order_id="123456"
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
# Amount is reduced by "fee"
|
# Amount is reduced by "fee"
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
|
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
|
||||||
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
||||||
@ -1452,7 +1485,7 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee
|
|||||||
open_rate=0.245441,
|
open_rate=0.245441,
|
||||||
open_order_id="123456"
|
open_order_id="123456"
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
# Amount is reduced by "fee"
|
# Amount is reduced by "fee"
|
||||||
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount - 0.004
|
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount - 0.004
|
||||||
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
||||||
@ -1480,7 +1513,7 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order
|
|||||||
open_rate=0.245441,
|
open_rate=0.245441,
|
||||||
open_order_id="123456"
|
open_order_id="123456"
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
# Amount does not change
|
# Amount does not change
|
||||||
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount
|
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount
|
||||||
|
|
||||||
@ -1505,6 +1538,31 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee,
|
|||||||
open_rate=0.245441,
|
open_rate=0.245441,
|
||||||
open_order_id="123456"
|
open_order_id="123456"
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
# Amount does not change
|
# Amount does not change
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_real_amount_open_trade(default_conf, mocker):
|
||||||
|
"""
|
||||||
|
Test get_real_amount condition trade.fee_open == 0 or order['status'] == 'open'
|
||||||
|
"""
|
||||||
|
patch_get_signal(mocker)
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
|
||||||
|
amount = 12345
|
||||||
|
trade = Trade(
|
||||||
|
pair='LTC/ETH',
|
||||||
|
amount=amount,
|
||||||
|
exchange='binance',
|
||||||
|
open_rate=0.245441,
|
||||||
|
open_order_id="123456"
|
||||||
|
)
|
||||||
|
order = {
|
||||||
|
'id': 'mocked_order',
|
||||||
|
'amount': amount,
|
||||||
|
'status': 'open',
|
||||||
|
}
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
assert freqtrade.get_real_amount(trade, order) == amount
|
||||||
|
@ -7,6 +7,7 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade import OperationalException
|
||||||
from freqtrade.main import main, set_loggers
|
from freqtrade.main import main, set_loggers
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
@ -60,7 +61,7 @@ def test_set_loggers() -> None:
|
|||||||
assert value2 is logging.INFO
|
assert value2 is logging.INFO
|
||||||
|
|
||||||
|
|
||||||
def test_main(mocker, caplog) -> None:
|
def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
"""
|
||||||
Test main() function
|
Test main() function
|
||||||
In this test we are skipping the while True loop by throwing an exception.
|
In this test we are skipping the while True loop by throwing an exception.
|
||||||
@ -68,26 +69,74 @@ def test_main(mocker, caplog) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.FreqtradeBot',
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
_init_modules=MagicMock(),
|
_init_modules=MagicMock(),
|
||||||
worker=MagicMock(
|
worker=MagicMock(side_effect=Exception),
|
||||||
side_effect=KeyboardInterrupt
|
|
||||||
),
|
|
||||||
clean=MagicMock(),
|
clean=MagicMock(),
|
||||||
)
|
)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: default_conf
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
args = ['-c', 'config.json.example']
|
args = ['-c', 'config.json.example']
|
||||||
|
|
||||||
# Test Main + the KeyboardInterrupt exception
|
# Test Main + the KeyboardInterrupt exception
|
||||||
with pytest.raises(SystemExit) as pytest_wrapped_e:
|
|
||||||
main(args)
|
|
||||||
log_has('Starting freqtrade', caplog.record_tuples)
|
|
||||||
log_has('Got SIGINT, aborting ...', caplog.record_tuples)
|
|
||||||
assert pytest_wrapped_e.type == SystemExit
|
|
||||||
assert pytest_wrapped_e.value.code == 42
|
|
||||||
|
|
||||||
# Test the BaseException case
|
|
||||||
mocker.patch(
|
|
||||||
'freqtrade.freqtradebot.FreqtradeBot.worker',
|
|
||||||
MagicMock(side_effect=BaseException)
|
|
||||||
)
|
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
main(args)
|
main(args)
|
||||||
log_has('Got fatal exception!', caplog.record_tuples)
|
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
||||||
|
assert log_has('Fatal exception!', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test main() function
|
||||||
|
In this test we are skipping the while True loop by throwing an exception.
|
||||||
|
"""
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
|
_init_modules=MagicMock(),
|
||||||
|
worker=MagicMock(side_effect=KeyboardInterrupt),
|
||||||
|
clean=MagicMock(),
|
||||||
|
)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: default_conf
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
|
args = ['-c', 'config.json.example']
|
||||||
|
|
||||||
|
# Test Main + the KeyboardInterrupt exception
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
main(args)
|
||||||
|
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
||||||
|
assert log_has('SIGINT received, aborting ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test main() function
|
||||||
|
In this test we are skipping the while True loop by throwing an exception.
|
||||||
|
"""
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
|
_init_modules=MagicMock(),
|
||||||
|
worker=MagicMock(side_effect=OperationalException('Oh snap!')),
|
||||||
|
clean=MagicMock(),
|
||||||
|
)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: default_conf
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
|
args = ['-c', 'config.json.example']
|
||||||
|
|
||||||
|
# Test Main + the KeyboardInterrupt exception
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
main(args)
|
||||||
|
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
||||||
|
assert log_has('Oh snap!', caplog.record_tuples)
|
||||||
|
@ -39,7 +39,7 @@ def test_datesarray_to_datetimearray(ticker_history):
|
|||||||
assert dates[0].minute == 50
|
assert dates[0].minute == 50
|
||||||
|
|
||||||
date_len = len(dates)
|
date_len = len(dates)
|
||||||
assert date_len == 3
|
assert date_len == 2
|
||||||
|
|
||||||
|
|
||||||
def test_common_datearray(default_conf) -> None:
|
def test_common_datearray(default_conf) -> None:
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
import os
|
from copy import deepcopy
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
|
from freqtrade import constants, OperationalException
|
||||||
from freqtrade.persistence import Trade, init, clean_dry_run_db
|
from freqtrade.persistence import Trade, init, clean_dry_run_db
|
||||||
|
|
||||||
|
|
||||||
@ -21,77 +23,54 @@ def test_init_create_session(default_conf, mocker):
|
|||||||
assert 'Session' in type(Trade.session).__name__
|
assert 'Session' in type(Trade.session).__name__
|
||||||
|
|
||||||
|
|
||||||
def test_init_dry_run_db(default_conf, mocker):
|
def test_init_custom_db_url(default_conf, mocker):
|
||||||
default_conf.update({'dry_run_db': True})
|
conf = deepcopy(default_conf)
|
||||||
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
|
|
||||||
|
|
||||||
# First, protect the existing 'tradesv3.dry_run.sqlite' (Do not delete user data)
|
# Update path to a value other than default, but still in-memory
|
||||||
dry_run_db = 'tradesv3.dry_run.sqlite'
|
conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
|
||||||
dry_run_db_swp = dry_run_db + '.swp'
|
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||||
|
mocker.patch.dict('freqtrade.persistence._CONF', conf)
|
||||||
|
|
||||||
if os.path.isfile(dry_run_db):
|
init(conf)
|
||||||
os.rename(dry_run_db, dry_run_db_swp)
|
assert create_engine_mock.call_count == 1
|
||||||
|
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
|
||||||
# Check if the new tradesv3.dry_run.sqlite was created
|
|
||||||
init(default_conf)
|
|
||||||
assert os.path.isfile(dry_run_db) is True
|
|
||||||
|
|
||||||
# Delete the file made for this unitest and rollback to the previous
|
|
||||||
# tradesv3.dry_run.sqlite file
|
|
||||||
|
|
||||||
# 1. Delete file from the test
|
|
||||||
if os.path.isfile(dry_run_db):
|
|
||||||
os.remove(dry_run_db)
|
|
||||||
|
|
||||||
# 2. Rollback to the initial file
|
|
||||||
if os.path.isfile(dry_run_db_swp):
|
|
||||||
os.rename(dry_run_db_swp, dry_run_db)
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_dry_run_without_db(default_conf, mocker):
|
def test_init_invalid_db_url(default_conf, mocker):
|
||||||
default_conf.update({'dry_run_db': False})
|
conf = deepcopy(default_conf)
|
||||||
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
|
|
||||||
|
|
||||||
# First, protect the existing 'tradesv3.dry_run.sqlite' (Do not delete user data)
|
# Update path to a value other than default, but still in-memory
|
||||||
dry_run_db = 'tradesv3.dry_run.sqlite'
|
conf.update({'db_url': 'unknown:///some.url'})
|
||||||
dry_run_db_swp = dry_run_db + '.swp'
|
mocker.patch.dict('freqtrade.persistence._CONF', conf)
|
||||||
|
|
||||||
if os.path.isfile(dry_run_db):
|
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
|
||||||
os.rename(dry_run_db, dry_run_db_swp)
|
init(conf)
|
||||||
|
|
||||||
# Check if the new tradesv3.dry_run.sqlite was created
|
|
||||||
init(default_conf)
|
|
||||||
assert os.path.isfile(dry_run_db) is False
|
|
||||||
|
|
||||||
# Rollback to the initial 'tradesv3.dry_run.sqlite' file
|
|
||||||
if os.path.isfile(dry_run_db_swp):
|
|
||||||
os.rename(dry_run_db_swp, dry_run_db)
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_prod_db(default_conf, mocker):
|
def test_init_prod_db(default_conf, mocker):
|
||||||
default_conf.update({'dry_run': False})
|
conf = deepcopy(default_conf)
|
||||||
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
|
conf.update({'dry_run': False})
|
||||||
|
conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
|
||||||
|
|
||||||
# First, protect the existing 'tradesv3.sqlite' (Do not delete user data)
|
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||||
prod_db = 'tradesv3.sqlite'
|
mocker.patch.dict('freqtrade.persistence._CONF', conf)
|
||||||
prod_db_swp = prod_db + '.swp'
|
|
||||||
|
|
||||||
if os.path.isfile(prod_db):
|
init(conf)
|
||||||
os.rename(prod_db, prod_db_swp)
|
assert create_engine_mock.call_count == 1
|
||||||
|
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
|
||||||
|
|
||||||
# Check if the new tradesv3.sqlite was created
|
|
||||||
init(default_conf)
|
|
||||||
assert os.path.isfile(prod_db) is True
|
|
||||||
|
|
||||||
# Delete the file made for this unitest and rollback to the previous tradesv3.sqlite file
|
def test_init_dryrun_db(default_conf, mocker):
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf.update({'dry_run': True})
|
||||||
|
conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
|
||||||
|
|
||||||
# 1. Delete file from the test
|
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||||
if os.path.isfile(prod_db):
|
mocker.patch.dict('freqtrade.persistence._CONF', conf)
|
||||||
os.remove(prod_db)
|
|
||||||
|
|
||||||
# Rollback to the initial 'tradesv3.sqlite' file
|
init(conf)
|
||||||
if os.path.isfile(prod_db_swp):
|
assert create_engine_mock.call_count == 1
|
||||||
os.rename(prod_db_swp, prod_db)
|
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
@ -328,7 +307,7 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee):
|
|||||||
|
|
||||||
|
|
||||||
def test_clean_dry_run_db(default_conf, fee):
|
def test_clean_dry_run_db(default_conf, fee):
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf)
|
||||||
|
|
||||||
# Simulate dry_run entries
|
# Simulate dry_run entries
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
@ -377,7 +356,7 @@ def test_clean_dry_run_db(default_conf, fee):
|
|||||||
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 1
|
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_migrate_old(default_conf, fee):
|
def test_migrate_old(mocker, default_conf, fee):
|
||||||
"""
|
"""
|
||||||
Test Database migration(starting with old pairformat)
|
Test Database migration(starting with old pairformat)
|
||||||
"""
|
"""
|
||||||
@ -409,11 +388,13 @@ def test_migrate_old(default_conf, fee):
|
|||||||
amount=amount
|
amount=amount
|
||||||
)
|
)
|
||||||
engine = create_engine('sqlite://')
|
engine = create_engine('sqlite://')
|
||||||
|
mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
|
||||||
|
|
||||||
# Create table using the old format
|
# Create table using the old format
|
||||||
engine.execute(create_table_old)
|
engine.execute(create_table_old)
|
||||||
engine.execute(insert_table_old)
|
engine.execute(insert_table_old)
|
||||||
# Run init to test migration
|
# Run init to test migration
|
||||||
init(default_conf, engine)
|
init(default_conf)
|
||||||
|
|
||||||
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
||||||
trade = Trade.query.filter(Trade.id == 1).first()
|
trade = Trade.query.filter(Trade.id == 1).first()
|
||||||
@ -428,7 +409,7 @@ def test_migrate_old(default_conf, fee):
|
|||||||
assert trade.exchange == "bittrex"
|
assert trade.exchange == "bittrex"
|
||||||
|
|
||||||
|
|
||||||
def test_migrate_new(default_conf, fee):
|
def test_migrate_new(mocker, default_conf, fee):
|
||||||
"""
|
"""
|
||||||
Test Database migration (starting with new pairformat)
|
Test Database migration (starting with new pairformat)
|
||||||
"""
|
"""
|
||||||
@ -460,11 +441,13 @@ def test_migrate_new(default_conf, fee):
|
|||||||
amount=amount
|
amount=amount
|
||||||
)
|
)
|
||||||
engine = create_engine('sqlite://')
|
engine = create_engine('sqlite://')
|
||||||
|
mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
|
||||||
|
|
||||||
# Create table using the old format
|
# Create table using the old format
|
||||||
engine.execute(create_table_old)
|
engine.execute(create_table_old)
|
||||||
engine.execute(insert_table_old)
|
engine.execute(insert_table_old)
|
||||||
# Run init to test migration
|
# Run init to test migration
|
||||||
init(default_conf, engine)
|
init(default_conf)
|
||||||
|
|
||||||
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
||||||
trade = Trade.query.filter(Trade.id == 1).first()
|
trade = Trade.query.filter(Trade.id == 1).first()
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
|
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
|
||||||
curl -O -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
|
||||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||||
cd ta-lib && ./configure && make && sudo make install && cd ..
|
cd ta-lib && ./configure && make && sudo make install && cd ..
|
||||||
else
|
else
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
ccxt==1.14.24
|
ccxt==1.14.169
|
||||||
SQLAlchemy==1.2.7
|
SQLAlchemy==1.2.8
|
||||||
python-telegram-bot==10.1.0
|
python-telegram-bot==10.1.0
|
||||||
arrow==0.12.1
|
arrow==0.12.1
|
||||||
cachetools==2.1.0
|
cachetools==2.1.0
|
||||||
@ -10,14 +10,14 @@ pandas==0.23.0
|
|||||||
scikit-learn==0.19.1
|
scikit-learn==0.19.1
|
||||||
scipy==1.1.0
|
scipy==1.1.0
|
||||||
jsonschema==2.6.0
|
jsonschema==2.6.0
|
||||||
numpy==1.14.3
|
numpy==1.14.4
|
||||||
TA-Lib==0.4.17
|
TA-Lib==0.4.17
|
||||||
pytest==3.5.1
|
pytest==3.6.1
|
||||||
pytest-mock==1.10.0
|
pytest-mock==1.10.0
|
||||||
pytest-cov==2.5.1
|
pytest-cov==2.5.1
|
||||||
hyperopt==0.1
|
hyperopt==0.1
|
||||||
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
|
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
|
||||||
networkx==1.11
|
networkx==1.11 # pyup: ignore
|
||||||
tabulate==0.8.2
|
tabulate==0.8.2
|
||||||
coinmarketcap==5.0.3
|
coinmarketcap==5.0.3
|
||||||
|
|
||||||
|
@ -8,24 +8,28 @@ import arrow
|
|||||||
|
|
||||||
from freqtrade import (exchange, arguments, misc)
|
from freqtrade import (exchange, arguments, misc)
|
||||||
|
|
||||||
DEFAULT_DL_PATH = 'freqtrade/tests/testdata'
|
DEFAULT_DL_PATH = 'user_data/data'
|
||||||
|
|
||||||
arguments = arguments.Arguments(sys.argv[1:], 'download utility')
|
arguments = arguments.Arguments(sys.argv[1:], 'download utility')
|
||||||
arguments.testdata_dl_options()
|
arguments.testdata_dl_options()
|
||||||
args = arguments.parse_args()
|
args = arguments.parse_args()
|
||||||
|
|
||||||
TICKER_INTERVALS = ['1m', '5m']
|
timeframes = args.timeframes
|
||||||
PAIRS = []
|
|
||||||
|
|
||||||
if args.pairs_file:
|
dl_path = os.path.join(DEFAULT_DL_PATH, args.exchange)
|
||||||
with open(args.pairs_file) as file:
|
if args.export:
|
||||||
PAIRS = json.load(file)
|
|
||||||
PAIRS = list(set(PAIRS))
|
|
||||||
|
|
||||||
dl_path = DEFAULT_DL_PATH
|
|
||||||
if args.export and os.path.exists(args.export):
|
|
||||||
dl_path = args.export
|
dl_path = args.export
|
||||||
|
|
||||||
|
if not os.path.isdir(dl_path):
|
||||||
|
sys.exit(f'Directory {dl_path} does not exist.')
|
||||||
|
|
||||||
|
pairs_file = args.pairs_file if args.pairs_file else os.path.join(dl_path, 'pairs.json')
|
||||||
|
if not os.path.isfile(pairs_file):
|
||||||
|
sys.exit(f'No pairs file found with path {pairs_file}.')
|
||||||
|
|
||||||
|
with open(pairs_file) as file:
|
||||||
|
PAIRS = list(set(json.load(file)))
|
||||||
|
|
||||||
since_time = None
|
since_time = None
|
||||||
if args.days:
|
if args.days:
|
||||||
since_time = arrow.utcnow().shift(days=-args.days).timestamp * 1000
|
since_time = arrow.utcnow().shift(days=-args.days).timestamp * 1000
|
||||||
@ -40,7 +44,7 @@ exchange._API = exchange.init_ccxt({'key': '',
|
|||||||
|
|
||||||
|
|
||||||
for pair in PAIRS:
|
for pair in PAIRS:
|
||||||
for tick_interval in TICKER_INTERVALS:
|
for tick_interval in timeframes:
|
||||||
print(f'downloading pair {pair}, interval {tick_interval}')
|
print(f'downloading pair {pair}, interval {tick_interval}')
|
||||||
|
|
||||||
data = exchange.get_ticker_history(pair, tick_interval, since_ms=since_time)
|
data = exchange.get_ticker_history(pair, tick_interval, since_ms=since_time)
|
||||||
|
@ -5,47 +5,75 @@ Script to display when the bot will buy a specific pair
|
|||||||
Mandatory Cli parameters:
|
Mandatory Cli parameters:
|
||||||
-p / --pair: pair to examine
|
-p / --pair: pair to examine
|
||||||
|
|
||||||
Optional Cli parameters
|
Option but recommended
|
||||||
-s / --strategy: strategy to use
|
-s / --strategy: strategy to use
|
||||||
|
|
||||||
|
|
||||||
|
Optional Cli parameters
|
||||||
-d / --datadir: path to pair backtest data
|
-d / --datadir: path to pair backtest data
|
||||||
--timerange: specify what timerange of data to use.
|
--timerange: specify what timerange of data to use.
|
||||||
-l / --live: Live, to download the latest ticker for the pair
|
-l / --live: Live, to download the latest ticker for the pair
|
||||||
-db / --db-url: Show trades stored in database
|
-db / --db-url: Show trades stored in database
|
||||||
|
|
||||||
|
|
||||||
|
Indicators recommended
|
||||||
|
Row 1: sma, ema3, ema5, ema10, ema50
|
||||||
|
Row 3: macd, rsi, fisher_rsi, mfi, slowd, slowk, fastd, fastk
|
||||||
|
|
||||||
|
Example of usage:
|
||||||
|
> python3 scripts/plot_dataframe.py --pair BTC/EUR -d user_data/data/ --indicators1 sma,ema3
|
||||||
|
--indicators2 fastk,fastd
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
|
from typing import Dict, List, Any
|
||||||
|
|
||||||
from typing import List
|
import plotly.graph_objs as go
|
||||||
|
|
||||||
from plotly import tools
|
from plotly import tools
|
||||||
from plotly.offline import plot
|
from plotly.offline import plot
|
||||||
import plotly.graph_objs as go
|
|
||||||
|
|
||||||
from typing import Dict, List, Any
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
|
|
||||||
from freqtrade.arguments import Arguments
|
|
||||||
from freqtrade.analyze import Analyze
|
|
||||||
from freqtrade import exchange
|
|
||||||
import freqtrade.optimize as optimize
|
import freqtrade.optimize as optimize
|
||||||
|
from freqtrade import exchange
|
||||||
from freqtrade import persistence
|
from freqtrade import persistence
|
||||||
|
from freqtrade.analyze import Analyze
|
||||||
|
from freqtrade.arguments import Arguments
|
||||||
|
from freqtrade.optimize.backtesting import setup_configuration
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
_CONF: Dict[str, Any] = {}
|
_CONF: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
def plot_analyzed_dataframe(args: Namespace) -> None:
|
def plot_analyzed_dataframe(args: Namespace) -> None:
|
||||||
"""
|
"""
|
||||||
Calls analyze() and plots the returned dataframe
|
Calls analyze() and plots the returned dataframe
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
pair = args.pair.replace('-', '_')
|
global _CONF
|
||||||
|
|
||||||
|
# Load the configuration
|
||||||
|
_CONF.update(setup_configuration(args))
|
||||||
|
|
||||||
|
# Set the pair to audit
|
||||||
|
pair = args.pair
|
||||||
|
|
||||||
|
if pair is None:
|
||||||
|
logger.critical('Parameter --pair mandatory;. E.g --pair ETH/BTC')
|
||||||
|
exit()
|
||||||
|
|
||||||
|
if '/' not in pair:
|
||||||
|
logger.critical('--pair format must be XXX/YYY')
|
||||||
|
exit()
|
||||||
|
|
||||||
|
# Set timerange to use
|
||||||
timerange = Arguments.parse_timerange(args.timerange)
|
timerange = Arguments.parse_timerange(args.timerange)
|
||||||
|
|
||||||
# Init strategy
|
# Load the strategy
|
||||||
try:
|
try:
|
||||||
analyze = Analyze({'strategy': args.strategy})
|
analyze = Analyze(_CONF)
|
||||||
|
exchange.init(_CONF)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logger.critical(
|
logger.critical(
|
||||||
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
|
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
|
||||||
@ -53,37 +81,75 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
|||||||
)
|
)
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
tick_interval = analyze.strategy.ticker_interval
|
# Set the ticker to use
|
||||||
|
tick_interval = analyze.get_ticker_interval()
|
||||||
|
|
||||||
|
# Load pair tickers
|
||||||
tickers = {}
|
tickers = {}
|
||||||
if args.live:
|
if args.live:
|
||||||
logger.info('Downloading pair.')
|
logger.info('Downloading pair.')
|
||||||
# Init Bittrex to use public API
|
|
||||||
exchange.init({'key': '', 'secret': ''})
|
|
||||||
tickers[pair] = exchange.get_ticker_history(pair, tick_interval)
|
tickers[pair] = exchange.get_ticker_history(pair, tick_interval)
|
||||||
else:
|
else:
|
||||||
tickers = optimize.load_data(
|
tickers = optimize.load_data(
|
||||||
datadir=args.datadir,
|
datadir=args.datadir,
|
||||||
pairs=[pair],
|
pairs=[pair],
|
||||||
ticker_interval=tick_interval,
|
ticker_interval=tick_interval,
|
||||||
refresh_pairs=False,
|
refresh_pairs=_CONF.get('refresh_pairs', False),
|
||||||
timerange=timerange
|
timerange=timerange
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# No ticker found, or impossible to download
|
||||||
|
if tickers == {}:
|
||||||
|
exit()
|
||||||
|
|
||||||
|
# Get trades already made from the DB
|
||||||
|
trades: List[Trade] = []
|
||||||
|
if args.db_url:
|
||||||
|
persistence.init(_CONF)
|
||||||
|
trades = Trade.query.filter(Trade.pair.is_(pair)).all()
|
||||||
|
|
||||||
dataframes = analyze.tickerdata_to_dataframe(tickers)
|
dataframes = analyze.tickerdata_to_dataframe(tickers)
|
||||||
dataframe = dataframes[pair]
|
dataframe = dataframes[pair]
|
||||||
dataframe = analyze.populate_buy_trend(dataframe)
|
dataframe = analyze.populate_buy_trend(dataframe)
|
||||||
dataframe = analyze.populate_sell_trend(dataframe)
|
dataframe = analyze.populate_sell_trend(dataframe)
|
||||||
|
|
||||||
trades = []
|
|
||||||
if args.db_url:
|
|
||||||
engine = create_engine('sqlite:///' + args.db_url)
|
|
||||||
persistence.init(_CONF, engine)
|
|
||||||
trades = Trade.query.filter(Trade.pair.is_(pair)).all()
|
|
||||||
|
|
||||||
if len(dataframe.index) > 750:
|
if len(dataframe.index) > 750:
|
||||||
logger.warning('Ticker contained more than 750 candles, clipping.')
|
logger.warning('Ticker contained more than 750 candles, clipping.')
|
||||||
data = dataframe.tail(750)
|
|
||||||
|
|
||||||
|
fig = generate_graph(
|
||||||
|
pair=pair,
|
||||||
|
trades=trades,
|
||||||
|
data=dataframe.tail(750),
|
||||||
|
args=args
|
||||||
|
)
|
||||||
|
|
||||||
|
plot(fig, filename=os.path.join('user_data', 'freqtrade-plot.html'))
|
||||||
|
|
||||||
|
|
||||||
|
def generate_graph(pair, trades, data, args) -> tools.make_subplots:
|
||||||
|
"""
|
||||||
|
Generate the graph from the data generated by Backtesting or from DB
|
||||||
|
:param pair: Pair to Display on the graph
|
||||||
|
:param trades: All trades created
|
||||||
|
:param data: Dataframe
|
||||||
|
:param args: sys.argv that contrains the two params indicators1, and indicators2
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define the graph
|
||||||
|
fig = tools.make_subplots(
|
||||||
|
rows=3,
|
||||||
|
cols=1,
|
||||||
|
shared_xaxes=True,
|
||||||
|
row_width=[1, 1, 4],
|
||||||
|
vertical_spacing=0.0001,
|
||||||
|
)
|
||||||
|
fig['layout'].update(title=pair)
|
||||||
|
fig['layout']['yaxis1'].update(title='Price')
|
||||||
|
fig['layout']['yaxis2'].update(title='Volume')
|
||||||
|
fig['layout']['yaxis3'].update(title='Other')
|
||||||
|
|
||||||
|
# Common information
|
||||||
candles = go.Candlestick(
|
candles = go.Candlestick(
|
||||||
x=data.date,
|
x=data.date,
|
||||||
open=data.open,
|
open=data.open,
|
||||||
@ -145,6 +211,10 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Row 1
|
||||||
|
fig.append_trace(candles, 1, 1)
|
||||||
|
|
||||||
|
if 'bb_lowerband' in data and 'bb_upperband' in data:
|
||||||
bb_lower = go.Scatter(
|
bb_lower = go.Scatter(
|
||||||
x=data.date,
|
x=data.date,
|
||||||
y=data.bb_lowerband,
|
y=data.bb_lowerband,
|
||||||
@ -159,35 +229,49 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
|||||||
fillcolor="rgba(0,176,246,0.2)",
|
fillcolor="rgba(0,176,246,0.2)",
|
||||||
line={'color': "transparent"},
|
line={'color': "transparent"},
|
||||||
)
|
)
|
||||||
macd = go.Scattergl(x=data['date'], y=data['macd'], name='MACD')
|
|
||||||
macdsignal = go.Scattergl(x=data['date'], y=data['macdsignal'], name='MACD signal')
|
|
||||||
volume = go.Bar(x=data['date'], y=data['volume'], name='Volume')
|
|
||||||
|
|
||||||
fig = tools.make_subplots(
|
|
||||||
rows=3,
|
|
||||||
cols=1,
|
|
||||||
shared_xaxes=True,
|
|
||||||
row_width=[1, 1, 4],
|
|
||||||
vertical_spacing=0.0001,
|
|
||||||
)
|
|
||||||
|
|
||||||
fig.append_trace(candles, 1, 1)
|
|
||||||
fig.append_trace(bb_lower, 1, 1)
|
fig.append_trace(bb_lower, 1, 1)
|
||||||
fig.append_trace(bb_upper, 1, 1)
|
fig.append_trace(bb_upper, 1, 1)
|
||||||
|
|
||||||
|
fig = generate_row(fig=fig, row=1, raw_indicators=args.indicators1, data=data)
|
||||||
fig.append_trace(buys, 1, 1)
|
fig.append_trace(buys, 1, 1)
|
||||||
fig.append_trace(sells, 1, 1)
|
fig.append_trace(sells, 1, 1)
|
||||||
fig.append_trace(volume, 2, 1)
|
|
||||||
fig.append_trace(macd, 3, 1)
|
|
||||||
fig.append_trace(macdsignal, 3, 1)
|
|
||||||
fig.append_trace(trade_buys, 1, 1)
|
fig.append_trace(trade_buys, 1, 1)
|
||||||
fig.append_trace(trade_sells, 1, 1)
|
fig.append_trace(trade_sells, 1, 1)
|
||||||
|
|
||||||
fig['layout'].update(title=args.pair)
|
# Row 2
|
||||||
fig['layout']['yaxis1'].update(title='Price')
|
volume = go.Bar(
|
||||||
fig['layout']['yaxis2'].update(title='Volume')
|
x=data['date'],
|
||||||
fig['layout']['yaxis3'].update(title='MACD')
|
y=data['volume'],
|
||||||
|
name='Volume'
|
||||||
|
)
|
||||||
|
fig.append_trace(volume, 2, 1)
|
||||||
|
|
||||||
plot(fig, filename='freqtrade-plot.html')
|
# Row 3
|
||||||
|
fig = generate_row(fig=fig, row=3, raw_indicators=args.indicators2, data=data)
|
||||||
|
|
||||||
|
return fig
|
||||||
|
|
||||||
|
|
||||||
|
def generate_row(fig, row, raw_indicators, data) -> tools.make_subplots:
|
||||||
|
"""
|
||||||
|
Generator all the indicator selected by the user for a specific row
|
||||||
|
"""
|
||||||
|
for indicator in raw_indicators.split(','):
|
||||||
|
if indicator in data:
|
||||||
|
scattergl = go.Scattergl(
|
||||||
|
x=data['date'],
|
||||||
|
y=data[indicator],
|
||||||
|
name=indicator
|
||||||
|
)
|
||||||
|
fig.append_trace(scattergl, row, 1)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
'Indicator "%s" ignored. Reason: This indicator is not found '
|
||||||
|
'in your strategy.',
|
||||||
|
indicator
|
||||||
|
)
|
||||||
|
|
||||||
|
return fig
|
||||||
|
|
||||||
|
|
||||||
def plot_parse_args(args: List[str]) -> Namespace:
|
def plot_parse_args(args: List[str]) -> Namespace:
|
||||||
@ -198,6 +282,24 @@ def plot_parse_args(args: List[str]) -> Namespace:
|
|||||||
"""
|
"""
|
||||||
arguments = Arguments(args, 'Graph dataframe')
|
arguments = Arguments(args, 'Graph dataframe')
|
||||||
arguments.scripts_options()
|
arguments.scripts_options()
|
||||||
|
arguments.parser.add_argument(
|
||||||
|
'--indicators1',
|
||||||
|
help='Set indicators from your strategy you want in the first row of the graph. Separate '
|
||||||
|
'them with a coma. E.g: ema3,ema5 (default: %(default)s)',
|
||||||
|
type=str,
|
||||||
|
default='sma,ema3,ema5',
|
||||||
|
dest='indicators1',
|
||||||
|
)
|
||||||
|
|
||||||
|
arguments.parser.add_argument(
|
||||||
|
'--indicators2',
|
||||||
|
help='Set indicators from your strategy you want in the third row of the graph. Separate '
|
||||||
|
'them with a coma. E.g: fastd,fastk (default: %(default)s)',
|
||||||
|
type=str,
|
||||||
|
default='macd',
|
||||||
|
dest='indicators2',
|
||||||
|
)
|
||||||
|
|
||||||
arguments.common_args_parser()
|
arguments.common_args_parser()
|
||||||
arguments.optimizer_shared_options(arguments.parser)
|
arguments.optimizer_shared_options(arguments.parser)
|
||||||
arguments.backtesting_options(arguments.parser)
|
arguments.backtesting_options(arguments.parser)
|
||||||
|
@ -8,9 +8,12 @@ Mandatory Cli parameters:
|
|||||||
Optional Cli parameters
|
Optional Cli parameters
|
||||||
-c / --config: specify configuration file
|
-c / --config: specify configuration file
|
||||||
-s / --strategy: strategy to use
|
-s / --strategy: strategy to use
|
||||||
--timerange: specify what timerange of data to use.
|
-d / --datadir: path to pair backtest data
|
||||||
|
--timerange: specify what timerange of data to use
|
||||||
|
--export-filename: Specify where the backtest export is located.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
@ -90,7 +93,18 @@ def plot_profit(args: Namespace) -> None:
|
|||||||
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
|
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
|
||||||
config.get('strategy')
|
config.get('strategy')
|
||||||
)
|
)
|
||||||
exit()
|
exit(1)
|
||||||
|
|
||||||
|
# Load the profits results
|
||||||
|
try:
|
||||||
|
filename = args.exportfilename
|
||||||
|
with open(filename) as file:
|
||||||
|
data = json.load(file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.critical(
|
||||||
|
'File "backtest-result.json" not found. This script require backtesting '
|
||||||
|
'results to run.\nPlease run a backtesting with the parameter --export.')
|
||||||
|
exit(1)
|
||||||
|
|
||||||
# Take pairs from the cli otherwise switch to the pair in the config file
|
# Take pairs from the cli otherwise switch to the pair in the config file
|
||||||
if args.pair:
|
if args.pair:
|
||||||
@ -140,18 +154,7 @@ def plot_profit(args: Namespace) -> None:
|
|||||||
num += 1
|
num += 1
|
||||||
avgclose /= num
|
avgclose /= num
|
||||||
|
|
||||||
# Load the profits results
|
# make an profits-growth array
|
||||||
# And make an profits-growth array
|
|
||||||
|
|
||||||
try:
|
|
||||||
filename = 'backtest-result.json'
|
|
||||||
with open(filename) as file:
|
|
||||||
data = json.load(file)
|
|
||||||
except FileNotFoundError:
|
|
||||||
logger.critical('File "backtest-result.json" not found. This script require backtesting '
|
|
||||||
'results to run.\nPlease run a backtesting with the parameter --export.')
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
pg = make_profit_array(data, num_iterations, min_date, tick_interval, filter_pairs)
|
pg = make_profit_array(data, num_iterations, min_date, tick_interval, filter_pairs)
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -184,7 +187,7 @@ def plot_profit(args: Namespace) -> None:
|
|||||||
)
|
)
|
||||||
fig.append_trace(pair_profit, 3, 1)
|
fig.append_trace(pair_profit, 3, 1)
|
||||||
|
|
||||||
plot(fig, filename='freqtrade-profit-plot.html')
|
plot(fig, filename=os.path.join('user_data', 'freqtrade-profit-plot.html'))
|
||||||
|
|
||||||
|
|
||||||
def define_index(min_date: int, max_date: int, interval: str) -> int:
|
def define_index(min_date: int, max_date: int, interval: str) -> int:
|
||||||
|
@ -2,3 +2,9 @@
|
|||||||
#ignore =
|
#ignore =
|
||||||
max-line-length = 100
|
max-line-length = 100
|
||||||
max-complexity = 12
|
max-complexity = 12
|
||||||
|
|
||||||
|
[mypy]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
[mypy-freqtrade.tests.*]
|
||||||
|
ignore_errors = True
|
||||||
|
2
setup.py
2
setup.py
@ -12,7 +12,7 @@ from freqtrade import __version__
|
|||||||
setup(name='freqtrade',
|
setup(name='freqtrade',
|
||||||
version=__version__,
|
version=__version__,
|
||||||
description='Simple High Frequency Trading Bot for crypto currencies',
|
description='Simple High Frequency Trading Bot for crypto currencies',
|
||||||
url='https://github.com/gcarq/freqtrade',
|
url='https://github.com/freqtrade/freqtrade',
|
||||||
author='gcarq and contributors',
|
author='gcarq and contributors',
|
||||||
author_email='michael.egger@tsn.at',
|
author_email='michael.egger@tsn.at',
|
||||||
license='GPLv3',
|
license='GPLv3',
|
||||||
|
93
setup.sh
93
setup.sh
@ -2,16 +2,17 @@
|
|||||||
#encoding=utf8
|
#encoding=utf8
|
||||||
|
|
||||||
function updateenv () {
|
function updateenv () {
|
||||||
echo "
|
echo "-------------------------"
|
||||||
-------------------------
|
echo "Update your virtual env"
|
||||||
Update your virtual env
|
echo "-------------------------"
|
||||||
-------------------------
|
|
||||||
"
|
|
||||||
source .env/bin/activate
|
source .env/bin/activate
|
||||||
pip3.6 install --upgrade pip
|
echo "pip3 install in-progress. Please wait..."
|
||||||
pip3 install -r requirements.txt --upgrade
|
pip3.6 install --quiet --upgrade pip
|
||||||
pip3 install -r requirements.txt
|
pip3 install --quiet -r requirements.txt --upgrade
|
||||||
pip3 install -e .
|
pip3 install --quiet -r requirements.txt
|
||||||
|
pip3 install --quiet -e .
|
||||||
|
echo "pip3 install completed"
|
||||||
|
echo
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install tab lib
|
# Install tab lib
|
||||||
@ -29,10 +30,11 @@ function install_macos () {
|
|||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
echo "Install Brew"
|
echo "Install Brew"
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
echo
|
|
||||||
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||||
fi
|
fi
|
||||||
brew install python3 wget ta-lib
|
brew install python3 wget ta-lib
|
||||||
|
|
||||||
|
test_and_fix_python_on_mac
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install bot Debian_ubuntu
|
# Install bot Debian_ubuntu
|
||||||
@ -54,7 +56,6 @@ function reset () {
|
|||||||
echo "----------------------------"
|
echo "----------------------------"
|
||||||
echo "Reset branch and virtual env"
|
echo "Reset branch and virtual env"
|
||||||
echo "----------------------------"
|
echo "----------------------------"
|
||||||
echo
|
|
||||||
if [ "1" == $(git branch -vv |grep -cE "\* develop|\* master") ]
|
if [ "1" == $(git branch -vv |grep -cE "\* develop|\* master") ]
|
||||||
then
|
then
|
||||||
if [ -d ".env" ]; then
|
if [ -d ".env" ]; then
|
||||||
@ -77,34 +78,53 @@ function reset () {
|
|||||||
echo "Reset ignored because you are not on 'master' or 'develop'."
|
echo "Reset ignored because you are not on 'master' or 'develop'."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
python3.6 -m venv .env
|
python3.6 -m venv .env
|
||||||
updateenv
|
updateenv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function test_and_fix_python_on_mac() {
|
||||||
|
|
||||||
|
if ! [ -x "$(command -v python3.6)" ]
|
||||||
|
then
|
||||||
|
echo "-------------------------"
|
||||||
|
echo "Fixing Python"
|
||||||
|
echo "-------------------------"
|
||||||
|
echo "Python 3.6 is not linked in your system. Fixing it..."
|
||||||
|
brew link --overwrite python
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
function config_generator () {
|
function config_generator () {
|
||||||
|
|
||||||
echo "Starting to generate config.json"
|
echo "Starting to generate config.json"
|
||||||
|
echo
|
||||||
echo "-------------------------"
|
|
||||||
echo "General configuration"
|
echo "General configuration"
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
|
default_max_trades=3
|
||||||
|
read -p "Max open trades: (Default: $default_max_trades) " max_trades
|
||||||
|
max_trades=${max_trades:-$default_max_trades}
|
||||||
|
|
||||||
|
default_stake_amount=0.05
|
||||||
|
read -p "Stake amount: (Default: $default_stake_amount) " stake_amount
|
||||||
|
stake_amount=${stake_amount:-$default_stake_amount}
|
||||||
|
|
||||||
|
default_stake_currency="BTC"
|
||||||
|
read -p "Stake currency: (Default: $default_stake_currency) " stake_currency
|
||||||
|
stake_currency=${stake_currency:-$default_stake_currency}
|
||||||
|
|
||||||
|
default_fiat_currency="USD"
|
||||||
|
read -p "Fiat currency: (Default: $default_fiat_currency) " fiat_currency
|
||||||
|
fiat_currency=${fiat_currency:-$default_fiat_currency}
|
||||||
|
|
||||||
echo
|
echo
|
||||||
read -p "Max open trades: (Default: 3) " max_trades
|
echo "Exchange config generator"
|
||||||
|
|
||||||
read -p "Stake amount: (Default: 0.05) " stake_amount
|
|
||||||
|
|
||||||
read -p "Stake currency: (Default: BTC) " stake_currency
|
|
||||||
|
|
||||||
read -p "Fiat currency: (Default: USD) " fiat_currency
|
|
||||||
|
|
||||||
echo "------------------------"
|
echo "------------------------"
|
||||||
echo "Bittrex config generator"
|
|
||||||
echo "------------------------"
|
|
||||||
echo
|
|
||||||
read -p "Exchange API key: " api_key
|
read -p "Exchange API key: " api_key
|
||||||
read -p "Exchange API Secret: " api_secret
|
read -p "Exchange API Secret: " api_secret
|
||||||
|
|
||||||
echo "-------------------------"
|
echo
|
||||||
echo "Telegram config generator"
|
echo "Telegram config generator"
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
read -p "Telegram Token: " token
|
read -p "Telegram Token: " token
|
||||||
@ -123,6 +143,10 @@ function config_generator () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function config () {
|
function config () {
|
||||||
|
|
||||||
|
echo "-------------------------"
|
||||||
|
echo "Config file generator"
|
||||||
|
echo "-------------------------"
|
||||||
if [ -f config.json ]
|
if [ -f config.json ]
|
||||||
then
|
then
|
||||||
read -p "A config file already exist, do you want to override it [Y/N]? "
|
read -p "A config file already exist, do you want to override it [Y/N]? "
|
||||||
@ -136,22 +160,26 @@ function config () {
|
|||||||
config_generator
|
config_generator
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-------------------------"
|
||||||
|
echo "Config file generated"
|
||||||
|
echo "-------------------------"
|
||||||
echo "Edit ./config.json to modify Pair and other configurations."
|
echo "Edit ./config.json to modify Pair and other configurations."
|
||||||
|
echo
|
||||||
}
|
}
|
||||||
|
|
||||||
function install () {
|
function install () {
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
echo "Install mandatory dependencies"
|
echo "Install mandatory dependencies"
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
echo
|
|
||||||
|
|
||||||
if [ "$(uname -s)" == "Darwin" ]
|
if [ "$(uname -s)" == "Darwin" ]
|
||||||
then
|
then
|
||||||
echo "- You are on macOS"
|
echo "macOS detected. Setup for this system in-progress"
|
||||||
install_macos
|
install_macos
|
||||||
elif [ -x "$(command -v apt-get)" ]
|
elif [ -x "$(command -v apt-get)" ]
|
||||||
then
|
then
|
||||||
echo "- You are on Debian/Ubuntu"
|
echo "Debian/Ubuntu detected. Setup for this system in-progress"
|
||||||
install_debian
|
install_debian
|
||||||
else
|
else
|
||||||
echo "This script does not support your OS."
|
echo "This script does not support your OS."
|
||||||
@ -159,12 +187,13 @@ function install () {
|
|||||||
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
|
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
|
||||||
sleep 10
|
sleep 10
|
||||||
fi
|
fi
|
||||||
|
echo
|
||||||
reset
|
reset
|
||||||
echo "
|
|
||||||
- Install complete.
|
|
||||||
"
|
|
||||||
config
|
config
|
||||||
echo "You can now use the bot by executing 'source .env/bin/activate; python3 freqtrade/main.py'."
|
echo "-------------------------"
|
||||||
|
echo "Run the bot"
|
||||||
|
echo "-------------------------"
|
||||||
|
echo "You can now use the bot by executing 'source .env/bin/activate; python3.6 freqtrade/main.py'."
|
||||||
}
|
}
|
||||||
|
|
||||||
function plot () {
|
function plot () {
|
||||||
|
BIN
ta-lib-0.4.0-src.tar.gz
Normal file
BIN
ta-lib-0.4.0-src.tar.gz
Normal file
Binary file not shown.
@ -14,7 +14,7 @@ import numpy # noqa
|
|||||||
class TestStrategy(IStrategy):
|
class TestStrategy(IStrategy):
|
||||||
"""
|
"""
|
||||||
This is a test strategy to inspire you.
|
This is a test strategy to inspire you.
|
||||||
More information in https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md
|
More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md
|
||||||
|
|
||||||
You can:
|
You can:
|
||||||
- Rename the class name (Do not forget to update class_name)
|
- Rename the class name (Do not forget to update class_name)
|
||||||
|
Loading…
Reference in New Issue
Block a user