Merge pull request #3905 from freqtrade/new_release

New release 2020.10
This commit is contained in:
Matthias 2020-10-30 20:15:57 +01:00 committed by GitHub
commit 4d4471480b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
157 changed files with 2545 additions and 1264 deletions

View File

@ -19,14 +19,14 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ ubuntu-18.04, macos-latest ] os: [ ubuntu-18.04, ubuntu-20.04, macos-latest ]
python-version: [3.7, 3.8] python-version: [3.7, 3.8]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v1 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@ -70,7 +70,7 @@ jobs:
pytest --random-order --cov=freqtrade --cov-config=.coveragerc pytest --random-order --cov=freqtrade --cov-config=.coveragerc
- name: Coveralls - name: Coveralls
if: (startsWith(matrix.os, 'ubuntu') && matrix.python-version == '3.8') if: (startsWith(matrix.os, 'ubuntu-20') && matrix.python-version == '3.8')
env: env:
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories # Coveralls token. Not used as secret due to github not providing secrets to forked repositories
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
@ -88,12 +88,16 @@ jobs:
run: | run: |
cp config.json.example config.json cp config.json.example config.json
freqtrade create-userdir --userdir user_data freqtrade create-userdir --userdir user_data
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --print-all freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all
- name: Flake8 - name: Flake8
run: | run: |
flake8 flake8
- name: Sort imports (isort)
run: |
isort --check .
- name: Mypy - name: Mypy
run: | run: |
mypy freqtrade scripts mypy freqtrade scripts
@ -121,7 +125,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v1 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@ -150,7 +154,7 @@ jobs:
run: | run: |
cp config.json.example config.json cp config.json.example config.json
freqtrade create-userdir --userdir user_data freqtrade create-userdir --userdir user_data
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --print-all freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all
- name: Flake8 - name: Flake8
run: | run: |
@ -172,7 +176,7 @@ jobs:
url: ${{ secrets.SLACK_WEBHOOK }} url: ${{ secrets.SLACK_WEBHOOK }}
docs_check: docs_check:
runs-on: ubuntu-latest runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -180,6 +184,17 @@ jobs:
run: | run: |
./tests/test_docs.sh ./tests/test_docs.sh
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Documentation build
run: |
pip install -r docs/requirements-docs.txt
pip install mkdocs
mkdocs build
- name: Slack Notification - name: Slack Notification
uses: homoluctus/slatify@v1.8.0 uses: homoluctus/slatify@v1.8.0
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
@ -190,7 +205,7 @@ jobs:
url: ${{ secrets.SLACK_WEBHOOK }} url: ${{ secrets.SLACK_WEBHOOK }}
cleanup-prior-runs: cleanup-prior-runs:
runs-on: ubuntu-latest runs-on: ubuntu-20.04
steps: steps:
- name: Cleanup previous runs on this branch - name: Cleanup previous runs on this branch
uses: rokroskar/workflow-run-cleanup-action@v0.2.2 uses: rokroskar/workflow-run-cleanup-action@v0.2.2
@ -201,7 +216,7 @@ jobs:
# Notify on slack only once - when CI completes (and after deploy) in case it's successfull # Notify on slack only once - when CI completes (and after deploy) in case it's successfull
notify-complete: notify-complete:
needs: [ build, build_windows, docs_check ] needs: [ build, build_windows, docs_check ]
runs-on: ubuntu-latest runs-on: ubuntu-20.04
steps: steps:
- name: Slack Notification - name: Slack Notification
uses: homoluctus/slatify@v1.8.0 uses: homoluctus/slatify@v1.8.0
@ -214,13 +229,13 @@ jobs:
deploy: deploy:
needs: [ build, build_windows, docs_check ] needs: [ build, build_windows, docs_check ]
runs-on: ubuntu-18.04 runs-on: ubuntu-20.04
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade' if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v1 uses: actions/setup-python@v2
with: with:
python-version: 3.8 python-version: 3.8

View File

@ -33,7 +33,7 @@ jobs:
- script: - script:
- cp config.json.example config.json - cp config.json.example config.json
- freqtrade create-userdir --userdir user_data - freqtrade create-userdir --userdir user_data
- freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily
name: hyperopt name: hyperopt
- script: flake8 - script: flake8
name: flake8 name: flake8

View File

@ -65,6 +65,14 @@ Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using
mypy freqtrade mypy freqtrade
``` ```
### 4. Ensure all imports are correct
#### Run isort
``` bash
isort .
```
## (Core)-Committer Guide ## (Core)-Committer Guide
### Process: Pull Requests ### Process: Pull Requests

View File

@ -55,9 +55,8 @@ Please find the complete documentation on our [website](https://www.freqtrade.io
Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot. Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot.
```bash ```bash
git clone git@github.com:freqtrade/freqtrade.git git clone -b develop https://github.com/freqtrade/freqtrade.git
cd freqtrade cd freqtrade
git checkout develop
./setup.sh --install ./setup.sh --install
``` ```
@ -111,17 +110,17 @@ optional arguments:
Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on our [documentation](https://www.freqtrade.io/en/latest/telegram-usage/) Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on our [documentation](https://www.freqtrade.io/en/latest/telegram-usage/)
- `/start`: Starts the trader - `/start`: Starts the trader.
- `/stop`: Stops the trader - `/stop`: Stops the trader.
- `/status [table]`: Lists all open trades - `/stopbuy`: Stop entering new trades.
- `/count`: Displays number of open trades - `/status [table]`: Lists all open trades.
- `/profit`: Lists cumulative profit from all finished trades - `/profit`: Lists cumulative profit from all finished trades
- `/forcesell <trade_id>|all`: Instantly sells the given trade (Ignoring `minimum_roi`). - `/forcesell <trade_id>|all`: Instantly sells the given trade (Ignoring `minimum_roi`).
- `/performance`: Show performance of each finished trade grouped by pair - `/performance`: Show performance of each finished trade grouped by pair
- `/balance`: Show account balance per currency - `/balance`: Show account balance per currency.
- `/daily <n>`: Shows profit or loss per day, over the last n days - `/daily <n>`: Shows profit or loss per day, over the last n days.
- `/help`: Show help message - `/help`: Show help message.
- `/version`: Show version - `/version`: Show version.
## Development branches ## Development branches
@ -133,13 +132,16 @@ The project is currently setup in two main branches:
## Support ## Support
### Help / Slack ### Help / Slack / Discord
For any questions not covered by the documentation or for further 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/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). - [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE).
Alternatively, check out the newly created [discord server](https://discord.gg/MA9v74M).
*Note*: Since the discord server is relatively new, answers to questions might be slightly delayed as currently the user base quite small.
### [Bugs / Issues](https://github.com/freqtrade/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
@ -166,7 +168,7 @@ Please read our
[Contributing document](https://github.com/freqtrade/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.
Coding is not a neccessity to contribute - maybe start with improving our documentation? Coding is not a necessity to contribute - maybe start with improving our documentation?
Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase. Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase.
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. **Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
@ -177,7 +179,7 @@ Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/
### Up-to-date clock ### Up-to-date clock
The clock must be accurate, syncronized to a NTP server very frequently to avoid problems with communication to the exchanges. The clock must be accurate, synchronized to a NTP server very frequently to avoid problems with communication to the exchanges.
### Min hardware required ### Min hardware required

Binary file not shown.

Binary file not shown.

View File

@ -7,10 +7,10 @@ python -m pip install --upgrade pip
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" $pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
if ($pyv -eq '3.7') { if ($pyv -eq '3.7') {
pip install build_helpers\TA_Lib-0.4.18-cp37-cp37m-win_amd64.whl pip install build_helpers\TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl
} }
if ($pyv -eq '3.8') { if ($pyv -eq '3.8') {
pip install build_helpers\TA_Lib-0.4.18-cp38-cp38-win_amd64.whl pip install build_helpers\TA_Lib-0.4.19-cp38-cp38-win_amd64.whl
} }
pip install -r requirements-dev.txt pip install -r requirements-dev.txt

View File

@ -5,15 +5,15 @@
"tradable_balance_ratio": 0.99, "tradable_balance_ratio": 0.99,
"fiat_display_currency": "USD", "fiat_display_currency": "USD",
"timeframe": "5m", "timeframe": "5m",
"dry_run": false, "dry_run": true,
"cancel_open_orders_on_exit": false, "cancel_open_orders_on_exit": false,
"unfilledtimeout": { "unfilledtimeout": {
"buy": 10, "buy": 10,
"sell": 30 "sell": 30
}, },
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0,
"use_order_book": false, "use_order_book": false,
"ask_last_balance": 0.0,
"order_book_top": 1, "order_book_top": 1,
"check_depth_of_market": { "check_depth_of_market": {
"enabled": false, "enabled": false,

View File

@ -7,7 +7,7 @@
"amount_reserve_percent": 0.05, "amount_reserve_percent": 0.05,
"amend_last_stake_amount": false, "amend_last_stake_amount": false,
"last_stake_amount_min_ratio": 0.5, "last_stake_amount_min_ratio": 0.5,
"dry_run": false, "dry_run": true,
"cancel_open_orders_on_exit": false, "cancel_open_orders_on_exit": false,
"timeframe": "5m", "timeframe": "5m",
"trailing_stop": false, "trailing_stop": false,

View File

@ -27,12 +27,11 @@
"use_sell_signal": true, "use_sell_signal": true,
"sell_profit_only": false, "sell_profit_only": false,
"ignore_roi_if_buy_signal": false "ignore_roi_if_buy_signal": false
}, },
"exchange": { "exchange": {
"name": "kraken", "name": "kraken",
"key": "", "key": "your_exchange_key",
"secret": "", "secret": "your_exchange_key",
"ccxt_config": {"enableRateLimit": true}, "ccxt_config": {"enableRateLimit": true},
"ccxt_async_config": { "ccxt_async_config": {
"enableRateLimit": true, "enableRateLimit": true,

View File

@ -5,6 +5,3 @@ FROM freqtradeorg/freqtrade:${sourceimage}
COPY requirements-plot.txt /freqtrade/ COPY requirements-plot.txt /freqtrade/
RUN pip install -r requirements-plot.txt --no-cache-dir RUN pip install -r requirements-plot.txt --no-cache-dir
# Empty the ENTRYPOINT to allow all commands
ENTRYPOINT []

View File

@ -27,9 +27,9 @@ class MyAwesomeHyperOpt2(MyAwesomeHyperOpt):
and then quickly switch between hyperopt classes, running optimization process with hyperopt class you need in each particular case: and then quickly switch between hyperopt classes, running optimization process with hyperopt class you need in each particular case:
``` ```
$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt ... $ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ...
or or
$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 ... $ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ...
``` ```
## Creating and using a custom loss function ## Creating and using a custom loss function

View File

@ -303,7 +303,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]]
[--dmmp] [--print-all] [--no-color] [--print-json] [--dmmp] [--print-all] [--no-color] [--print-json]
[-j JOBS] [--random-state INT] [--min-trades INT] [-j JOBS] [--random-state INT] [--min-trades INT]
[--continue] [--hyperopt-loss NAME] [--hyperopt-loss NAME]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -349,18 +349,14 @@ optional arguments:
reproducible hyperopt results. reproducible hyperopt results.
--min-trades INT Set minimal desired number of trades for evaluations --min-trades INT Set minimal desired number of trades for evaluations
in the hyperopt optimization path (default: 1). in the hyperopt optimization path (default: 1).
--continue Continue hyperopt from previous runs. By default,
temporary files will be removed and hyperopt will
start from scratch.
--hyperopt-loss NAME Specify the class name of the hyperopt loss function --hyperopt-loss NAME Specify the class name of the hyperopt loss function
class (IHyperOptLoss). Different functions can class (IHyperOptLoss). Different functions can
generate completely different results, since the generate completely different results, since the
target for optimization is different. Built-in target for optimization is different. Built-in
Hyperopt-loss-functions are: DefaultHyperOptLoss, Hyperopt-loss-functions are: ShortTradeDurHyperOptLoss,
OnlyProfitHyperOptLoss, SharpeHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss,
SharpeHyperOptLossDaily, SortinoHyperOptLoss, SharpeHyperOptLossDaily, SortinoHyperOptLoss,
SortinoHyperOptLossDaily.(default: SortinoHyperOptLossDaily.
`DefaultHyperOptLoss`).
Common arguments: Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).

View File

@ -59,8 +59,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float | `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float | `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer | `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer | `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`). | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`).
| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook-enabled). | `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook-enabled).
| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean | `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean
@ -574,144 +574,7 @@ Assuming both buy and sell are using market orders, a configuration similar to t
``` ```
Obviously, if only one side is using limit orders, different pricing combinations can be used. Obviously, if only one side is using limit orders, different pricing combinations can be used.
--8<-- "includes/pairlists.md"
## Pairlists and Pairlist Handlers
Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler).
Additionaly, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler.
Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist.
### Available Pairlist Handlers
* [`StaticPairList`](#static-pair-list) (default, if not configured differently)
* [`VolumePairList`](#volume-pair-list)
* [`AgeFilter`](#agefilter)
* [`PrecisionFilter`](#precisionfilter)
* [`PriceFilter`](#pricefilter)
* [`ShuffleFilter`](#shufflefilter)
* [`SpreadFilter`](#spreadfilter)
!!! Tip "Testing pairlists"
Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) utility subcommand to test your configuration quickly.
#### Static Pair List
By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration.
It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`.
```json
"pairlists": [
{"method": "StaticPairList"}
],
```
#### Volume Pair List
`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`).
When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume.
When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange.
The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes).
`VolumePairList` is based on the ticker data from exchange, as reported by the ccxt library:
* The `quoteVolume` is the amount of quote (stake) currency traded (bought or sold) in last 24 hours.
```json
"pairlists": [{
"method": "VolumePairList",
"number_assets": 20,
"sort_key": "quoteVolume",
"refresh_period": 1800,
}],
```
#### AgeFilter
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`).
When pairs are first listed on an exchange they can suffer huge price drops and volatility
in the first few days while the pair goes through its price-discovery period. Bots can often
be caught out buying before the pair has finished dropping in price.
This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days.
#### PrecisionFilter
Filters low-value coins which would not allow setting stoplosses.
#### PriceFilter
The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported:
* `min_price`
* `max_price`
* `low_price_ratio`
The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs.
This option is disabled by default, and will only apply if set to > 0.
The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs.
This option is disabled by default, and will only apply if set to > 0.
The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio.
This option is disabled by default, and will only apply if set to > 0.
For `PriceFiler` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied.
Calculation example:
Min price precision for SHITCOIN/BTC is 8 decimals. If its price is 0.00000011 - one price step above would be 0.00000012, which is ~9% higher than the previous price value. You may filter out this pair by using PriceFilter with `low_price_ratio` set to 0.09 (9%) or with `min_price` set to 0.00000011, correspondingly.
!!! Warning "Low priced pairs"
Low priced pairs with high "1 pip movements" are dangerous since they are often illiquid and it may also be impossible to place the desired stoploss, which can often result in high losses since price needs to be rounded to the next tradable price - so instead of having a stoploss of -5%, you could end up with a stoploss of -9% simply due to price rounding.
#### ShuffleFilter
Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority.
!!! Tip
You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order.
#### SpreadFilter
Removes pairs that have a difference between asks and bids above the specified ratio, `max_spread_ratio` (defaults to `0.005`).
Example:
If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005` and this pair will be filtered out.
### Full example of Pairlist Handlers
The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 priceunit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value.
```json
"exchange": {
"pair_whitelist": [],
"pair_blacklist": ["BNB/BTC"]
},
"pairlists": [
{
"method": "VolumePairList",
"number_assets": 20,
"sort_key": "quoteVolume",
},
{"method": "AgeFilter", "min_days_listed": 10},
{"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.01},
{"method": "SpreadFilter", "max_spread_ratio": 0.005},
{"method": "ShuffleFilter", "seed": 42}
],
```
## Switch to Dry-run mode ## Switch to Dry-run mode

View File

@ -96,7 +96,7 @@ Below is an outline of exception inheritance hierarchy:
## Modules ## Modules
### Dynamic Pairlist ### Pairlists
You have a great idea for a new pair selection algorithm you would like to try out? Great. You have a great idea for a new pair selection algorithm you would like to try out? Great.
Hopefully you also want to contribute this back upstream. Hopefully you also want to contribute this back upstream.

View File

@ -82,20 +82,34 @@ Risk Reward Ratio ($R$) is a formula used to measure the expected gains of a giv
$$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$
???+ Example "Worked example of $R$ calculation" ???+ Example "Worked example of $R$ calculation"
Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin, it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100.<br> Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin, it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100, which will give you 10 shares (100 / 10).
Your potential profit is calculated as:<br>
Your potential profit is calculated as:
$\begin{aligned} $\begin{aligned}
\text{potential_profit} &= (\text{potential_price} - \text{cost_per_unit}) * \frac{\text{investment}}{\text{cost_per_unit}} \\ \text{potential_profit} &= (\text{potential_price} - \text{entry_price}) * \frac{\text{investment}}{\text{entry_price}} \\
&= (15 - 10) * \frac{100}{15}\\ &= (15 - 10) * (100 / 10) \\
&= 33.33 &= 50
\end{aligned}$<br> \end{aligned}$
Since the price might go to $0, the $100 dolars invested could turn into 0. We can compute the Risk Reward Ratio as follows:<br>
Since the price might go to $0, the $100 dollars invested could turn into 0.
We do however use a stoploss of 15% - so in the worst case, we'll sell 15% below entry price (or at 8.5$).
$\begin{aligned}
\text{potential_loss} &= (\text{entry_price} - \text{stoploss}) * \frac{\text{investment}}{\text{entry_price}} \\
&= (10 - 8.5) * (100 / 10)\\
&= 15
\end{aligned}$
We can compute the Risk Reward Ratio as follows:
$\begin{aligned} $\begin{aligned}
R &= \frac{\text{potential_profit}}{\text{potential_loss}}\\ R &= \frac{\text{potential_profit}}{\text{potential_loss}}\\
&= \frac{33.33}{100}\\ &= \frac{50}{15}\\
&= 0.333... &= 3.33
\end{aligned}$<br> \end{aligned}$<br>
What it effectivelly means is that the strategy have the potential to make $0.33 for each $1 invested. What it effectively means is that the strategy have the potential to make 3.33$ for each $1 invested.
On a long horizon, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows: On a long horizon, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows:

View File

@ -140,13 +140,7 @@ Since hyperopt uses Bayesian search, running for too many epochs may not produce
It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going. It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going.
```bash ```bash
freqtrade hyperopt -e 1000 freqtrade hyperopt --hyperop SampleHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000
```
or if you want intermediate result to see
```bash
for i in {1..100}; do freqtrade hyperopt -e 1000; done
``` ```
### Why does it take a long time to run hyperopt? ### Why does it take a long time to run hyperopt?

View File

@ -37,12 +37,20 @@ pip install -r requirements-hyperopt.txt
Before we start digging into Hyperopt, we recommend you to take a look at Before we start digging into Hyperopt, we recommend you to take a look at
the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt.py). the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt.py).
Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar and a lot of code can be copied across from the strategy. Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar.
The simplest way to get started is to use `freqtrade new-hyperopt --hyperopt AwesomeHyperopt`. !!! Tip "About this page"
This will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class.
### Checklist on all tasks / possibilities in hyperopt The simplest way to get started is to use the following, command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`.
``` bash
freqtrade new-hyperopt --hyperopt AwesomeHyperopt
```
### Hyperopt checklist
Checklist on all tasks / possibilities in hyperopt
Depending on the space you want to optimize, only some of the below are required: Depending on the space you want to optimize, only some of the below are required:
@ -54,17 +62,15 @@ Depending on the space you want to optimize, only some of the below are required
!!! Note !!! Note
`populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work.
Optional - can also be loaded from a strategy: Optional in hyperopt - can also be loaded from a strategy (recommended):
* copy `populate_indicators` from your strategy - otherwise default-strategy will be used * copy `populate_indicators` from your strategy - otherwise default-strategy will be used
* copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used * copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used
* copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used * copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used
!!! Note
Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead.
!!! Note !!! Note
You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods.
Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead.
Rarely you may also need to override: Rarely you may also need to override:
@ -80,17 +86,20 @@ Rarely you may also need to override:
# Have a working strategy at hand. # Have a working strategy at hand.
freqtrade new-hyperopt --hyperopt EmptyHyperopt freqtrade new-hyperopt --hyperopt EmptyHyperopt
freqtrade hyperopt --hyperopt EmptyHyperopt --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 freqtrade hyperopt --hyperopt EmptyHyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100
``` ```
### 1. Install a Custom Hyperopt File ### Create a Custom Hyperopt File
Put your hyperopt file into the directory `user_data/hyperopts`. Let assume you want a hyperopt file `AwesomeHyperopt.py`:
Let assume you want a hyperopt file `awesome_hyperopt.py`: ``` bash
Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts/awesome_hyperopt.py` freqtrade new-hyperopt --hyperopt AwesomeHyperopt
```
### 2. Configure your Guards and Triggers This command will create a new hyperopt file from a template, allowing you to get started quickly.
### Configure your Guards and Triggers
There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing: There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing:
@ -102,14 +111,16 @@ There you have two different types of indicators: 1. `guards` and 2. `triggers`.
1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10. 1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10.
2. Triggers are ones that actually trigger buy in specific moment, like "buy when EMA5 crosses over EMA10" or "buy when close price touches lower Bollinger band". 2. Triggers are ones that actually trigger buy in specific moment, like "buy when EMA5 crosses over EMA10" or "buy when close price touches lower Bollinger band".
Hyperoptimization will, for each eval round, pick one trigger and possibly !!! Hint "Guards and Triggers"
multiple guards. The constructed strategy will be something like Technically, there is no difference between Guards and Triggers.
"*buy exactly when close price touches lower Bollinger band, BUT only if However, this guide will make this distinction to make it clear that signals should not be "sticking".
Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning).
Hyper-optimization will, for each epoch round, pick one trigger and possibly
multiple guards. The constructed strategy will be something like "*buy exactly when close price touches lower Bollinger band, BUT only if
ADX > 10*". ADX > 10*".
If you have updated the buy strategy, i.e. changed the contents of If you have updated the buy strategy, i.e. changed the contents of `populate_buy_trend()` method, you have to update the `guards` and `triggers` your hyperopt must use correspondingly.
`populate_buy_trend()` method, you have to update the `guards` and
`triggers` your hyperopt must use correspondingly.
#### Sell optimization #### Sell optimization
@ -126,7 +137,7 @@ To avoid naming collisions in the search-space, please prefix all sell-spaces wi
The Strategy class exposes the timeframe value as the `self.timeframe` attribute. The Strategy class exposes the timeframe value as the `self.timeframe` attribute.
The same value is available as class-attribute `HyperoptName.timeframe`. The same value is available as class-attribute `HyperoptName.timeframe`.
In the case of the linked sample-value this would be `SampleHyperOpt.timeframe`. In the case of the linked sample-value this would be `AwesomeHyperopt.timeframe`.
## Solving a Mystery ## Solving a Mystery
@ -192,27 +203,25 @@ So let's write the buy strategy using these values:
return populate_buy_trend return populate_buy_trend
``` ```
Hyperopting will now call this `populate_buy_trend` as many times you ask it (`epochs`) Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations.
with different value combinations. It will then use the given historical data and make It will use the given historical data and make buys based on the buy signals generated with the above function.
buys based on the buy signals generated with the above function and based on the results Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)).
it will end with telling you which parameter combination produced the best profits.
The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. !!! Note
When you want to test an indicator that isn't used by the bot currently, remember to The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators.
add it to the `populate_indicators()` method in your custom hyperopt file. When you want to test an indicator that isn't used by the bot currently, remember to
add it to the `populate_indicators()` method in your strategy or hyperopt file.
## Loss-functions ## Loss-functions
Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results.
By default, Freqtrade uses a loss function, which has been with freqtrade since the beginning and optimizes mostly for short trade duration and avoiding losses. A loss function must be specified via the `--hyperopt-loss <Class-name>` argument (or optionally via the configuration under the `"hyperopt_loss"` key).
A different loss function can be specified by using the `--hyperopt-loss <Class-name>` argument.
This class should be in its own file within the `user_data/hyperopts/` directory. This class should be in its own file within the `user_data/hyperopts/` directory.
Currently, the following loss functions are builtin: Currently, the following loss functions are builtin:
* `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) * `ShortTradeDurHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses.
* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration) * `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration)
* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation) * `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation)
* `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation) * `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation)
@ -229,21 +238,20 @@ Because hyperopt tries a lot of combinations to find the best parameters it will
We strongly recommend to use `screen` or `tmux` to prevent any connection loss. We strongly recommend to use `screen` or `tmux` to prevent any connection loss.
```bash ```bash
freqtrade hyperopt --config config.json --hyperopt <hyperoptname> -e 500 --spaces all freqtrade hyperopt --config config.json --hyperopt <hyperoptname> --hyperopt-loss <hyperoptlossname> --strategy <strategyname> -e 500 --spaces all
``` ```
Use `<hyperoptname>` as the name of the custom hyperopt used. Use `<hyperoptname>` as the name of the custom hyperopt used.
The `-e` option will set how many evaluations hyperopt will do. We recommend The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs.
running at least several thousand evaluations. Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results.
The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below.
!!! Note !!! Note
By default, hyperopt will erase previous results and start from scratch. Continuation can be archived by using `--continue`. Hyperopt will store hyperopt results with the timestamp of the hyperopt start time.
Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename <filename>` to read and display older hyperopt results.
!!! Warning You can find a list of filenames with `ls -l user_data/hyperopt_results/`.
When switching parameters or changing configuration options, make sure to not use the argument `--continue` so temporary results can be removed.
### Execute Hyperopt with different historical data source ### Execute Hyperopt with different historical data source
@ -251,13 +259,13 @@ If you would like to hyperopt parameters using an alternate historical data set
you have on-disk, use the `--datadir PATH` option. By default, hyperopt you have on-disk, use the `--datadir PATH` option. By default, hyperopt
uses data from directory `user_data/data`. uses data from directory `user_data/data`.
### Running Hyperopt with Smaller Testset ### Running Hyperopt with a smaller test-set
Use the `--timerange` argument to change how much of the testset you want to use. Use the `--timerange` argument to change how much of the test-set you want to use.
For example, to use one month of data, pass the following parameter to the hyperopt call: For example, to use one month of data, pass the following parameter to the hyperopt call:
```bash ```bash
freqtrade hyperopt --timerange 20180401-20180501 freqtrade hyperopt --hyperopt <hyperoptname> --strategy <strategyname> --timerange 20180401-20180501
``` ```
### Running Hyperopt using methods from a strategy ### Running Hyperopt using methods from a strategy
@ -265,16 +273,15 @@ freqtrade hyperopt --timerange 20180401-20180501
Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided.
```bash ```bash
freqtrade hyperopt --strategy SampleStrategy --hyperopt SampleHyperopt freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy
``` ```
### Running Hyperopt with Smaller Search Space ### Running Hyperopt with Smaller Search Space
Use the `--spaces` option to limit the search space used by hyperopt. Use the `--spaces` option to limit the search space used by hyperopt.
Letting Hyperopt optimize everything is a huuuuge search space. Often it Letting Hyperopt optimize everything is a huuuuge search space.
might make more sense to start by just searching for initial buy algorithm. Often it might make more sense to start by just searching for initial buy algorithm.
Or maybe you just want to optimize your stoploss or roi table for that awesome Or maybe you just want to optimize your stoploss or roi table for that awesome new buy strategy you have.
new buy strategy you have.
Legal values are: Legal values are:
@ -318,7 +325,7 @@ The initial state for generation of these random values (random state) is contro
If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used. If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used.
If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyperoptimization results with same random state value used. If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyper-optimization results with same random state value used.
## Understand the Hyperopt Result ## Understand the Hyperopt Result
@ -371,7 +378,7 @@ By default, hyperopt prints colorized results -- epochs with positive profit are
You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option.
!!! Note "Windows and color output" !!! Note "Windows and color output"
Windows does not support color-output nativly, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL.
### Understand Hyperopt ROI results ### Understand Hyperopt ROI results
@ -419,7 +426,9 @@ These ranges should be sufficient in most cases. The minutes in the steps (ROI d
If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default.
Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps).
A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py).
### Understand Hyperopt Stoploss results ### Understand Hyperopt Stoploss results
@ -441,7 +450,7 @@ Stoploss: -0.27996
In order to use this best stoploss value found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `stoploss` attribute of your custom strategy: In order to use this best stoploss value found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `stoploss` attribute of your custom strategy:
``` ``` python
# Optimal stoploss designed for the strategy # Optimal stoploss designed for the strategy
# This attribute will be overridden if the config file contains "stoploss" # This attribute will be overridden if the config file contains "stoploss"
stoploss = -0.27996 stoploss = -0.27996
@ -475,7 +484,7 @@ Trailing stop:
In order to use these best trailing stop parameters found by Hyperopt in backtesting and for live trades/dry-run, copy-paste them as the values of the corresponding attributes of your custom strategy: In order to use these best trailing stop parameters found by Hyperopt in backtesting and for live trades/dry-run, copy-paste them as the values of the corresponding attributes of your custom strategy:
``` ``` python
# Trailing stop # Trailing stop
# These attributes will be overridden if the config file contains corresponding values. # These attributes will be overridden if the config file contains corresponding values.
trailing_stop = True trailing_stop = True
@ -494,10 +503,14 @@ Override the `trailing_space()` method and define the desired range in it if you
## Show details of Hyperopt results ## Show details of Hyperopt results
After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` subcommands. The usage of these subcommands is described in the [Utils](utils.md#list-hyperopt-results) chapter. After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter.
## Validate backtesting results ## Validate backtesting results
Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected.
To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same set of arguments `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
Should results don't match, please double-check to make sure you transferred all conditions correctly.
Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy.
You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`).

137
docs/includes/pairlists.md Normal file
View File

@ -0,0 +1,137 @@
## Pairlists and Pairlist Handlers
Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler).
Additionally, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler.
Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist.
### Available Pairlist Handlers
* [`StaticPairList`](#static-pair-list) (default, if not configured differently)
* [`VolumePairList`](#volume-pair-list)
* [`AgeFilter`](#agefilter)
* [`PrecisionFilter`](#precisionfilter)
* [`PriceFilter`](#pricefilter)
* [`ShuffleFilter`](#shufflefilter)
* [`SpreadFilter`](#spreadfilter)
!!! Tip "Testing pairlists"
Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) utility sub-command to test your configuration quickly.
#### Static Pair List
By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration.
It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`.
```json
"pairlists": [
{"method": "StaticPairList"}
],
```
#### Volume Pair List
`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`).
When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume.
When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange.
The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes).
`VolumePairList` is based on the ticker data from exchange, as reported by the ccxt library:
* The `quoteVolume` is the amount of quote (stake) currency traded (bought or sold) in last 24 hours.
```json
"pairlists": [{
"method": "VolumePairList",
"number_assets": 20,
"sort_key": "quoteVolume",
"refresh_period": 1800,
}],
```
#### AgeFilter
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`).
When pairs are first listed on an exchange they can suffer huge price drops and volatility
in the first few days while the pair goes through its price-discovery period. Bots can often
be caught out buying before the pair has finished dropping in price.
This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days.
#### PrecisionFilter
Filters low-value coins which would not allow setting stoplosses.
#### PriceFilter
The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported:
* `min_price`
* `max_price`
* `low_price_ratio`
The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs.
This option is disabled by default, and will only apply if set to > 0.
The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs.
This option is disabled by default, and will only apply if set to > 0.
The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio.
This option is disabled by default, and will only apply if set to > 0.
For `PriceFiler` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied.
Calculation example:
Min price precision for SHITCOIN/BTC is 8 decimals. If its price is 0.00000011 - one price step above would be 0.00000012, which is ~9% higher than the previous price value. You may filter out this pair by using PriceFilter with `low_price_ratio` set to 0.09 (9%) or with `min_price` set to 0.00000011, correspondingly.
!!! Warning "Low priced pairs"
Low priced pairs with high "1 pip movements" are dangerous since they are often illiquid and it may also be impossible to place the desired stoploss, which can often result in high losses since price needs to be rounded to the next tradable price - so instead of having a stoploss of -5%, you could end up with a stoploss of -9% simply due to price rounding.
#### ShuffleFilter
Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority.
!!! Tip
You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order.
#### SpreadFilter
Removes pairs that have a difference between asks and bids above the specified ratio, `max_spread_ratio` (defaults to `0.005`).
Example:
If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005` and this pair will be filtered out.
### Full example of Pairlist Handlers
The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value.
```json
"exchange": {
"pair_whitelist": [],
"pair_blacklist": ["BNB/BTC"]
},
"pairlists": [
{
"method": "VolumePairList",
"number_assets": 20,
"sort_key": "quoteVolume",
},
{"method": "AgeFilter", "min_days_listed": 10},
{"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.01},
{"method": "SpreadFilter", "max_spread_ratio": 0.005},
{"method": "ShuffleFilter", "seed": 42}
],
```

View File

@ -59,11 +59,17 @@ Alternatively
## Support ## Support
### Help / Slack ### Help / Slack / Discord
For any questions not covered by the documentation or for further information about the bot, we encourage you to join our passionate Slack community. For any questions not covered by the documentation or for further information about the bot, we encourage you to join our passionate Slack community.
Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) to join the Freqtrade Slack channel. Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) to join the Freqtrade Slack channel.
Alternatively, check out the newly created [discord server](https://discord.gg/MA9v74M).
!!! Note
Since the discord server is relatively new, answers to questions might be slightly delayed as currently the user base quite small.
## Ready to try? ## Ready to try?
Begin by reading our installation guide [for docker](docker.md), or for [installation without docker](installation.md). Begin by reading our installation guide [for docker](docker.md) (recommended), or for [installation without docker](installation.md).

View File

@ -1,2 +1,3 @@
mkdocs-material==5.5.13 mkdocs-material==6.1.0
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2
pymdown-extensions==8.0.1

View File

@ -104,32 +104,42 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use
python3 scripts/rest_client.py --config rest_config.json <command> [optional parameters] python3 scripts/rest_client.py --config rest_config.json <command> [optional parameters]
``` ```
## Available commands ## Available endpoints
| Command | Description | | Command | Description |
|----------|-------------| |----------|-------------|
| `ping` | Simple command testing the API Readiness - requires no authentication. | `ping` | Simple command testing the API Readiness - requires no authentication.
| `start` | Starts the trader | `start` | Starts the trader.
| `stop` | Stops the trader | `stop` | Stops the trader.
| `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
| `reload_config` | Reloads the configuration file | `reload_config` | Reloads the configuration file.
| `trades` | List last trades. | `trades` | List last trades.
| `delete_trade <trade_id>` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `delete_trade <trade_id>` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange.
| `show_config` | Shows part of the current configuration with relevant settings to operation | `show_config` | Shows part of the current configuration with relevant settings to operation.
| `logs` | Shows last log messages | `logs` | Shows last log messages.
| `status` | Lists all open trades | `status` | Lists all open trades.
| `count` | Displays number of trades used and available | `count` | Displays number of trades used and available.
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance | `locks` | Displays currently locked pairs.
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance.
| `forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`). | `forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
| `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). | `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
| `forcebuy <pair> [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True) | `forcebuy <pair> [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
| `performance` | Show performance of each finished trade grouped by pair | `performance` | Show performance of each finished trade grouped by pair.
| `balance` | Show account balance per currency | `balance` | Show account balance per currency.
| `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7) | `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7).
| `whitelist` | Show the current whitelist | `whitelist` | Show the current whitelist.
| `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
| `edge` | Show validated pairs by Edge if it is enabled. | `edge` | Show validated pairs by Edge if it is enabled.
| `version` | Show version | `pair_candles` | Returns dataframe for a pair / timeframe combination while the bot is running. **Alpha**
| `pair_history` | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy. **Alpha**
| `plot_config` | Get plot config from the strategy (or nothing if not configured). **Alpha**
| `strategies` | List strategies in strategy directory. **Alpha**
| `strategy <strategy>` | Get specific Strategy content. **Alpha**
| `available_pairs` | List available backtest data. **Alpha**
| `version` | Show version.
!!! Warning "Alpha status"
Endpoints labeled with *Alpha status* above may change at any time without notice.
Possible commands can be listed from the rest-client script using the `help` command. Possible commands can be listed from the rest-client script using the `help` command.
@ -140,6 +150,12 @@ python3 scripts/rest_client.py help
``` output ``` output
Possible commands: Possible commands:
available_pairs
Return available pair (backtest data) based on timeframe / stake_currency selection
:param timeframe: Only pairs with this timeframe available.
:param stake_currency: Only pairs that include this timeframe
balance balance
Get the account balance. Get the account balance.
@ -179,9 +195,27 @@ logs
:param limit: Limits log messages to the last <limit> logs. No limit to get all the trades. :param limit: Limits log messages to the last <limit> logs. No limit to get all the trades.
pair_candles
Return live dataframe for <pair><timeframe>.
:param pair: Pair to get data for
:param timeframe: Only pairs with this timeframe available.
:param limit: Limit result to the last n candles.
pair_history
Return historic, analyzed dataframe
:param pair: Pair to get data for
:param timeframe: Only pairs with this timeframe available.
:param strategy: Strategy to analyze and get values for
:param timerange: Timerange to get data for (same format than --timerange endpoints)
performance performance
Return the performance of the different coins. Return the performance of the different coins.
plot_config
Return plot configuration if the strategy defines one.
profit profit
Return the profit summary. Return the profit summary.
@ -204,6 +238,14 @@ stop
stopbuy stopbuy
Stop buying (but handle sells gracefully). Use `reload_config` to reset. Stop buying (but handle sells gracefully). Use `reload_config` to reset.
strategies
Lists available strategies
strategy
Get strategy details
:param strategy: Strategy class name
trades trades
Return trades history. Return trades history.
@ -215,7 +257,6 @@ version
whitelist whitelist
Show the current whitelist. Show the current whitelist.
``` ```
## Advanced API usage using JWT tokens ## Advanced API usage using JWT tokens

View File

@ -43,52 +43,6 @@ sqlite3
.schema <table_name> .schema <table_name>
``` ```
### Trade table structure
```sql
CREATE TABLE trades(
id INTEGER NOT NULL,
exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL,
fee_open FLOAT NOT NULL,
fee_open_cost FLOAT,
fee_open_currency VARCHAR,
fee_close FLOAT NOT NULL,
fee_close_cost FLOAT,
fee_close_currency VARCHAR,
open_rate FLOAT,
open_rate_requested FLOAT,
open_trade_price FLOAT,
close_rate FLOAT,
close_rate_requested FLOAT,
close_profit FLOAT,
close_profit_abs FLOAT,
stake_amount FLOAT NOT NULL,
amount FLOAT,
open_date DATETIME NOT NULL,
close_date DATETIME,
open_order_id VARCHAR,
stop_loss FLOAT,
stop_loss_pct FLOAT,
initial_stop_loss FLOAT,
initial_stop_loss_pct FLOAT,
stoploss_order_id VARCHAR,
stoploss_last_update DATETIME,
max_rate FLOAT,
min_rate FLOAT,
sell_reason VARCHAR,
strategy VARCHAR,
timeframe INTEGER,
PRIMARY KEY (id),
CHECK (is_open IN (0, 1))
);
CREATE INDEX ix_trades_stoploss_order_id ON trades (stoploss_order_id);
CREATE INDEX ix_trades_pair ON trades (pair);
CREATE INDEX ix_trades_is_open ON trades (is_open);
```
## Get all trades in the table ## Get all trades in the table
```sql ```sql
@ -128,23 +82,12 @@ SET is_open=0,
WHERE id=31; WHERE id=31;
``` ```
## Manually insert a new trade
```sql
INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date)
VALUES ('binance', 'ETH/BTC', 1, 0.0025, 0.0025, <open_rate>, <stake_amount>, <amount>, '<datetime>')
```
### Insert trade example
```sql
INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date)
VALUES ('binance', 'ETH/BTC', 1, 0.0025, 0.0025, 0.00258580, 0.002, 0.7715262081, '2020-06-28 12:44:24.000000')
```
## Remove trade from the database ## Remove trade from the database
Maybe you'd like to remove a trade from the database, because something went wrong. !!! Tip "Use RPC Methods to delete trades"
Consider using `/delete <tradeid>` via telegram or rest API. That's the recommended way to deleting trades.
If you'd still like to remove a trade from the database directly, you can use the below query.
```sql ```sql
DELETE FROM trades WHERE id = <tradeid>; DELETE FROM trades WHERE id = <tradeid>;

View File

@ -312,12 +312,17 @@ The name of the variable can be chosen at will, but should be prefixed with `cus
class Awesomestrategy(IStrategy): class Awesomestrategy(IStrategy):
# Create custom dictionary # Create custom dictionary
cust_info = {} cust_info = {}
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Check if the entry already exists # Check if the entry already exists
if not metadata["pair"] in self._cust_info:
# Create empty entry for this pair
self._cust_info[metadata["pair"]] = {}
if "crosstime" in self.cust_info[metadata["pair"]: if "crosstime" in self.cust_info[metadata["pair"]:
self.cust_info[metadata["pair"]["crosstime"] += 1 self.cust_info[metadata["pair"]]["crosstime"] += 1
else: else:
self.cust_info[metadata["pair"]["crosstime"] = 1 self.cust_info[metadata["pair"]]["crosstime"] = 1
``` ```
!!! Warning !!! Warning
@ -688,18 +693,18 @@ Locked pairs will show the message `Pair <pair> is currently locked.`.
Sometimes it may be desired to lock a pair after certain events happen (e.g. multiple losing trades in a row). Sometimes it may be desired to lock a pair after certain events happen (e.g. multiple losing trades in a row).
Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until)`. Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until, [reason])`.
`until` must be a datetime object in the future, after which trading will be reenabled for that pair. `until` must be a datetime object in the future, after which trading will be re-enabled for that pair, while `reason` is an optional string detailing why the pair was locked.
Locks can also be lifted manually, by calling `self.unlock_pair(pair)`. Locks can also be lifted manually, by calling `self.unlock_pair(pair)`.
To verify if a pair is currently locked, use `self.is_pair_locked(pair)`. To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
!!! Note !!! Note
Locked pairs are not persisted, so a restart of the bot, or calling `/reload_config` will reset locked pairs. Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished.
!!! Warning !!! Warning
Locking pairs is not functioning during backtesting. Locking pairs is not available during backtesting.
#### Pair locking example #### Pair locking example

View File

@ -423,7 +423,7 @@ freqtrade test-pairlist --config config.json --quote USDT BTC
## List Hyperopt results ## List Hyperopt results
You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` subcommand. You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` sub-command.
``` ```
usage: freqtrade hyperopt-list [-h] [-v] [--logfile FILE] [-V] [-c PATH] usage: freqtrade hyperopt-list [-h] [-v] [--logfile FILE] [-V] [-c PATH]
@ -432,10 +432,11 @@ usage: freqtrade hyperopt-list [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[--max-trades INT] [--min-avg-time FLOAT] [--max-trades INT] [--min-avg-time FLOAT]
[--max-avg-time FLOAT] [--min-avg-profit FLOAT] [--max-avg-time FLOAT] [--min-avg-profit FLOAT]
[--max-avg-profit FLOAT] [--max-avg-profit FLOAT]
[--min-total-profit FLOAT] [--max-total-profit FLOAT] [--min-total-profit FLOAT]
[--max-total-profit FLOAT]
[--min-objective FLOAT] [--max-objective FLOAT] [--min-objective FLOAT] [--max-objective FLOAT]
[--no-color] [--print-json] [--no-details] [--no-color] [--print-json] [--no-details]
[--export-csv FILE] [--hyperopt-filename PATH] [--export-csv FILE]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -443,24 +444,27 @@ optional arguments:
--profitable Select only profitable epochs. --profitable Select only profitable epochs.
--min-trades INT Select epochs with more than INT trades. --min-trades INT Select epochs with more than INT trades.
--max-trades INT Select epochs with less than INT trades. --max-trades INT Select epochs with less than INT trades.
--min-avg-time FLOAT Select epochs on above average time. --min-avg-time FLOAT Select epochs above average time.
--max-avg-time FLOAT Select epochs on under average time. --max-avg-time FLOAT Select epochs below average time.
--min-avg-profit FLOAT --min-avg-profit FLOAT
Select epochs on above average profit. Select epochs above average profit.
--max-avg-profit FLOAT --max-avg-profit FLOAT
Select epochs on below average profit. Select epochs below average profit.
--min-total-profit FLOAT --min-total-profit FLOAT
Select epochs on above total profit. Select epochs above total profit.
--max-total-profit FLOAT --max-total-profit FLOAT
Select epochs on below total profit. Select epochs below total profit.
--min-objective FLOAT --min-objective FLOAT
Select epochs on above objective (- is added by default). Select epochs above objective.
--max-objective FLOAT --max-objective FLOAT
Select epochs on below objective (- is added by default). Select epochs below objective.
--no-color Disable colorization of hyperopt results. May be --no-color Disable colorization of hyperopt results. May be
useful if you are redirecting output to a file. useful if you are redirecting output to a file.
--print-json Print best result detailization in JSON format. --print-json Print output in JSON format.
--no-details Do not print best epoch details. --no-details Do not print best epoch details.
--hyperopt-filename FILENAME
Hyperopt result filename.Example: `--hyperopt-
filename=hyperopt_results_2020-09-27_16-20-48.pickle`
--export-csv FILE Export to CSV-File. This will disable table print. --export-csv FILE Export to CSV-File. This will disable table print.
Example: --export-csv hyperopt.csv Example: --export-csv hyperopt.csv
@ -481,6 +485,10 @@ Common arguments:
Path to userdata directory. Path to userdata directory.
``` ```
!!! Note
`hyperopt-list` will automatically use the latest available hyperopt results file.
You can override this using the `--hyperopt-filename` argument, and specify another, available filename (without path!).
### Examples ### Examples
List all results, print details of the best result at the end: List all results, print details of the best result at the end:
@ -501,17 +509,41 @@ You can show the details of any hyperoptimization epoch previously evaluated by
usage: freqtrade hyperopt-show [-h] [-v] [--logfile FILE] [-V] [-c PATH] usage: freqtrade hyperopt-show [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH] [--best] [-d PATH] [--userdir PATH] [--best]
[--profitable] [-n INT] [--print-json] [--profitable] [-n INT] [--print-json]
[--no-header] [--hyperopt-filename PATH] [--no-header]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
--best Select only best epochs. --best Select only best epochs.
--profitable Select only profitable epochs. --profitable Select only profitable epochs.
-n INT, --index INT Specify the index of the epoch to print details for. -n INT, --index INT Specify the index of the epoch to print details for.
--print-json Print best result detailization in JSON format. --print-json Print output in JSON format.
--hyperopt-filename FILENAME
Hyperopt result filename.Example: `--hyperopt-
filename=hyperopt_results_2020-09-27_16-20-48.pickle`
--no-header Do not print epoch details header. --no-header Do not print epoch details header.
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. Special values are:
'syslog', 'journald'. See the documentation for more
details.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default:
`userdir/config.json` or `config.json` whichever
exists). Multiple --config options may be used. Can be
set to `-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
``` ```
!!! Note
`hyperopt-show` will automatically use the latest available hyperopt results file.
You can override this using the `--hyperopt-filename` argument, and specify another, available filename (without path!).
### Examples ### Examples
Print details for the epoch 168 (the number of the epoch is shown by the `hyperopt-list` subcommand or by Hyperopt itself during hyperoptimization run): Print details for the epoch 168 (the number of the epoch is shown by the `hyperopt-list` subcommand or by Hyperopt itself during hyperoptimization run):

View File

@ -21,7 +21,7 @@ git clone https://github.com/freqtrade/freqtrade.git
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows). 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 unofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib0.4.18cp38cp38win_amd64.whl` (make sure to use the version matching your python version) As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib0.4.19cp38cp38win_amd64.whl` (make sure to use the version matching your python version)
Freqtrade provides these dependencies for the latest 2 Python versions (3.7 and 3.8) and for 64bit Windows. Freqtrade provides these dependencies for the latest 2 Python versions (3.7 and 3.8) and for 64bit Windows.
Other versions must be downloaded from the above link. Other versions must be downloaded from the above link.
@ -32,7 +32,7 @@ python -m venv .env
.env\Scripts\activate.ps1 .env\Scripts\activate.ps1
# optionally install ta-lib from wheel # optionally install ta-lib from wheel
# Eventually adjust the below filename to match the downloaded wheel # Eventually adjust the below filename to match the downloaded wheel
pip install build_helpes/TA_Lib0.4.18cp38cp38win_amd64.whl pip install build_helpes/TA_Lib0.4.19cp38cp38win_amd64.whl
pip install -r requirements.txt pip install -r requirements.txt
pip install -e . pip install -e .
freqtrade freqtrade

View File

@ -1,5 +1,5 @@
""" Freqtrade bot """ """ Freqtrade bot """
__version__ = '2020.9.1' __version__ = '2020.10'
if __version__ == 'develop': if __version__ == 'develop':

View File

@ -8,5 +8,6 @@ To launch Freqtrade as a module
from freqtrade import main from freqtrade import main
if __name__ == '__main__': if __name__ == '__main__':
main.main() main.main()

View File

@ -8,23 +8,15 @@ Note: Be careful with file-scoped imports in these subfiles.
""" """
from freqtrade.commands.arguments import Arguments from freqtrade.commands.arguments import Arguments
from freqtrade.commands.build_config_commands import start_new_config from freqtrade.commands.build_config_commands import start_new_config
from freqtrade.commands.data_commands import (start_convert_data, from freqtrade.commands.data_commands import (start_convert_data, start_download_data,
start_download_data,
start_list_data) start_list_data)
from freqtrade.commands.deploy_commands import (start_create_userdir, from freqtrade.commands.deploy_commands import (start_create_userdir, start_new_hyperopt,
start_new_hyperopt,
start_new_strategy) start_new_strategy)
from freqtrade.commands.hyperopt_commands import (start_hyperopt_list, from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show
start_hyperopt_show) from freqtrade.commands.list_commands import (start_list_exchanges, start_list_hyperopts,
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_markets, start_list_strategies,
start_list_hyperopts, start_list_timeframes, start_show_trades)
start_list_markets, from freqtrade.commands.optimize_commands import start_backtesting, start_edge, start_hyperopt
start_list_strategies,
start_list_timeframes,
start_show_trades)
from freqtrade.commands.optimize_commands import (start_backtesting,
start_edge, start_hyperopt)
from freqtrade.commands.pairlist_commands import start_test_pairlist from freqtrade.commands.pairlist_commands import start_test_pairlist
from freqtrade.commands.plot_commands import (start_plot_dataframe, from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
start_plot_profit)
from freqtrade.commands.trade_commands import start_trading from freqtrade.commands.trade_commands import start_trading

View File

@ -9,6 +9,7 @@ from typing import Any, Dict, List, Optional
from freqtrade.commands.cli_options import AVAILABLE_CLI_OPTIONS from freqtrade.commands.cli_options import AVAILABLE_CLI_OPTIONS
from freqtrade.constants import DEFAULT_CONFIG from freqtrade.constants import DEFAULT_CONFIG
ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"] ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"]
ARGS_STRATEGY = ["strategy", "strategy_path"] ARGS_STRATEGY = ["strategy", "strategy_path"]
@ -26,7 +27,7 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
"use_max_market_positions", "print_all", "use_max_market_positions", "print_all",
"print_colorized", "print_json", "hyperopt_jobs", "print_colorized", "print_json", "hyperopt_jobs",
"hyperopt_random_state", "hyperopt_min_trades", "hyperopt_random_state", "hyperopt_min_trades",
"hyperopt_continue", "hyperopt_loss"] "hyperopt_loss"]
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
@ -75,10 +76,10 @@ ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable",
"hyperopt_list_min_total_profit", "hyperopt_list_max_total_profit", "hyperopt_list_min_total_profit", "hyperopt_list_max_total_profit",
"hyperopt_list_min_objective", "hyperopt_list_max_objective", "hyperopt_list_min_objective", "hyperopt_list_max_objective",
"print_colorized", "print_json", "hyperopt_list_no_details", "print_colorized", "print_json", "hyperopt_list_no_details",
"export_csv"] "hyperoptexportfilename", "export_csv"]
ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index", ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index",
"print_json", "hyperopt_show_no_header"] "print_json", "hyperoptexportfilename", "hyperopt_show_no_header"]
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes", NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
"list-markets", "list-pairs", "list-strategies", "list-data", "list-markets", "list-pairs", "list-strategies", "list-data",
@ -161,16 +162,14 @@ class Arguments:
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot') self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
self._build_args(optionlist=['version'], parser=self.parser) self._build_args(optionlist=['version'], parser=self.parser)
from freqtrade.commands import (start_create_userdir, start_convert_data, from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir,
start_download_data, start_list_data, start_download_data, start_edge, start_hyperopt,
start_hyperopt_list, start_hyperopt_show, start_hyperopt_list, start_hyperopt_show, start_list_data,
start_list_exchanges, start_list_hyperopts, start_list_exchanges, start_list_hyperopts,
start_list_markets, start_list_strategies, start_list_markets, start_list_strategies,
start_list_timeframes, start_new_config, start_list_timeframes, start_new_config, start_new_hyperopt,
start_new_hyperopt, start_new_strategy, start_new_strategy, start_plot_dataframe, start_plot_profit,
start_plot_dataframe, start_plot_profit, start_show_trades, start_show_trades, start_test_pairlist, start_trading)
start_backtesting, start_hyperopt, start_edge,
start_test_pairlist, start_trading)
subparsers = self.parser.add_subparsers(dest='command', subparsers = self.parser.add_subparsers(dest='command',
# Use custom message when no subhandler is added # Use custom message when no subhandler is added

View File

@ -1,13 +1,15 @@
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Any, Dict from typing import Any, Dict, List
from questionary import Separator, prompt from questionary import Separator, prompt
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
from freqtrade.exchange import available_exchanges, MAP_EXCHANGE_CHILDCLASS
from freqtrade.misc import render_template
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges
from freqtrade.misc import render_template
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -46,7 +48,7 @@ def ask_user_config() -> Dict[str, Any]:
Interactive questions built using https://github.com/tmbo/questionary Interactive questions built using https://github.com/tmbo/questionary
:returns: Dict with keys to put into template :returns: Dict with keys to put into template
""" """
questions = [ questions: List[Dict[str, Any]] = [
{ {
"type": "confirm", "type": "confirm",
"name": "dry_run", "name": "dry_run",

View File

@ -4,6 +4,7 @@ Definition of cli arguments used in arguments.py
from argparse import ArgumentTypeError from argparse import ArgumentTypeError
from freqtrade import __version__, constants from freqtrade import __version__, constants
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN
def check_int_positive(value: str) -> int: def check_int_positive(value: str) -> int:
@ -252,23 +253,19 @@ AVAILABLE_CLI_OPTIONS = {
metavar='INT', metavar='INT',
default=1, default=1,
), ),
"hyperopt_continue": Arg(
"--continue",
help="Continue hyperopt from previous runs. "
"By default, temporary files will be removed and hyperopt will start from scratch.",
default=False,
action='store_true',
),
"hyperopt_loss": Arg( "hyperopt_loss": Arg(
'--hyperopt-loss', '--hyperopt-loss',
help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). '
'Different functions can generate completely different results, ' 'Different functions can generate completely different results, '
'since the target for optimization is different. Built-in Hyperopt-loss-functions are: ' 'since the target for optimization is different. Built-in Hyperopt-loss-functions are: '
'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, ' f'{", ".join(HYPEROPT_LOSS_BUILTIN)}',
'SortinoHyperOptLoss, SortinoHyperOptLossDaily.'
'(default: `%(default)s`).',
metavar='NAME', metavar='NAME',
default=constants.DEFAULT_HYPEROPT_LOSS, ),
"hyperoptexportfilename": Arg(
'--hyperopt-filename',
help='Hyperopt result filename.'
'Example: `--hyperopt-filename=hyperopt_results_2020-09-27_16-20-48.pickle`',
metavar='FILENAME',
), ),
# List exchanges # List exchanges
"print_one_column": Arg( "print_one_column": Arg(

View File

@ -6,16 +6,15 @@ from typing import Any, Dict, List
import arrow import arrow
from freqtrade.configuration import TimeRange, setup_utils_configuration from freqtrade.configuration import TimeRange, setup_utils_configuration
from freqtrade.data.converter import (convert_ohlcv_format, from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
convert_trades_format) from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data,
from freqtrade.data.history import (convert_trades_to_ohlcv,
refresh_backtest_ohlcv_data,
refresh_backtest_trades_data) refresh_backtest_trades_data)
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver
from freqtrade.state import RunMode from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -36,6 +35,9 @@ def start_download_data(args: Dict[str, Any]) -> None:
if 'timerange' in config: if 'timerange' in config:
timerange = timerange.parse_timerange(config['timerange']) timerange = timerange.parse_timerange(config['timerange'])
# Remove stake-currency to skip checks which are not relevant for datadownload
config['stake_currency'] = ''
if 'pairs' not in config: if 'pairs' not in config:
raise OperationalException( raise OperationalException(
"Downloading data requires a list of pairs. " "Downloading data requires a list of pairs. "
@ -105,8 +107,9 @@ def start_list_data(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
from freqtrade.data.history.idatahandler import get_datahandler
from tabulate import tabulate from tabulate import tabulate
from freqtrade.data.history.idatahandler import get_datahandler
dhc = get_datahandler(config['datadir'], config['dataformat_ohlcv']) dhc = get_datahandler(config['datadir'], config['dataformat_ohlcv'])
paircombs = dhc.ohlcv_get_available_data(config['datadir']) paircombs = dhc.ohlcv_get_available_data(config['datadir'])

View File

@ -4,13 +4,13 @@ from pathlib import Path
from typing import Any, Dict from typing import Any, Dict
from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration import setup_utils_configuration
from freqtrade.configuration.directory_operations import (copy_sample_files, from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir
create_userdata_dir)
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import render_template, render_template_with_fallback from freqtrade.misc import render_template, render_template_with_fallback
from freqtrade.state import RunMode from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -133,7 +133,7 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None:
if new_path.exists(): if new_path.exists():
raise OperationalException(f"`{new_path}` already exists. " raise OperationalException(f"`{new_path}` already exists. "
"Please choose another Strategy Name.") "Please choose another Hyperopt Name.")
deploy_new_hyperopt(args['hyperopt'], new_path, args['template']) deploy_new_hyperopt(args['hyperopt'], new_path, args['template'])
else: else:
raise OperationalException("`new-hyperopt` requires --hyperopt to be set.") raise OperationalException("`new-hyperopt` requires --hyperopt to be set.")

View File

@ -5,9 +5,11 @@ from typing import Any, Dict, List
from colorama import init as colorama_init from colorama import init as colorama_init
from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration import setup_utils_configuration
from freqtrade.data.btanalysis import get_latest_hyperopt_file
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.state import RunMode from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -40,8 +42,9 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
'filter_max_objective': config.get('hyperopt_list_max_objective', None), 'filter_max_objective': config.get('hyperopt_list_max_objective', None),
} }
results_file = (config['user_data_dir'] / results_file = get_latest_hyperopt_file(
'hyperopt_results' / 'hyperopt_results.pickle') config['user_data_dir'] / 'hyperopt_results',
config.get('hyperoptexportfilename'))
# Previous evaluations # Previous evaluations
epochs = Hyperopt.load_previous_results(results_file) epochs = Hyperopt.load_previous_results(results_file)
@ -80,8 +83,10 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
print_json = config.get('print_json', False) print_json = config.get('print_json', False)
no_header = config.get('hyperopt_show_no_header', False) no_header = config.get('hyperopt_show_no_header', False)
results_file = (config['user_data_dir'] / results_file = get_latest_hyperopt_file(
'hyperopt_results' / 'hyperopt_results.pickle') config['user_data_dir'] / 'hyperopt_results',
config.get('hyperoptexportfilename'))
n = config.get('hyperopt_show_index', -1) n = config.get('hyperopt_show_index', -1)
filteroptions = { filteroptions = {

View File

@ -5,20 +5,20 @@ from collections import OrderedDict
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List from typing import Any, Dict, List
from colorama import init as colorama_init
from colorama import Fore, Style
import rapidjson import rapidjson
from colorama import Fore, Style
from colorama import init as colorama_init
from tabulate import tabulate from tabulate import tabulate
from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration import setup_utils_configuration
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import (available_exchanges, ccxt_exchanges, from freqtrade.exchange import available_exchanges, ccxt_exchanges, market_is_active
market_is_active)
from freqtrade.misc import plural from freqtrade.misc import plural
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.state import RunMode from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -203,15 +203,16 @@ def start_show_trades(args: Dict[str, Any]) -> None:
""" """
Show trades Show trades
""" """
from freqtrade.persistence import init, Trade
import json import json
from freqtrade.persistence import Trade, init_db
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
if 'db_url' not in config: if 'db_url' not in config:
raise OperationalException("--db-url is required for this command.") raise OperationalException("--db-url is required for this command.")
logger.info(f'Using DB: "{config["db_url"]}"') logger.info(f'Using DB: "{config["db_url"]}"')
init(config['db_url'], clean_open_orders=False) init_db(config['db_url'], clean_open_orders=False)
tfilter = [] tfilter = []
if config.get('trade_ids'): if config.get('trade_ids'):

View File

@ -6,6 +6,7 @@ from freqtrade.configuration import setup_utils_configuration
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.state import RunMode from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -58,6 +59,7 @@ def start_hyperopt(args: Dict[str, Any]) -> None:
# Import here to avoid loading hyperopt module when it's not used # Import here to avoid loading hyperopt module when it's not used
try: try:
from filelock import FileLock, Timeout from filelock import FileLock, Timeout
from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt import Hyperopt
except ImportError as e: except ImportError as e:
raise OperationalException( raise OperationalException(
@ -98,6 +100,7 @@ def start_edge(args: Dict[str, Any]) -> None:
:return: None :return: None
""" """
from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.optimize.edge_cli import EdgeCli
# Initialize configuration # Initialize configuration
config = setup_optimize_configuration(args, RunMode.EDGE) config = setup_optimize_configuration(args, RunMode.EDGE)
logger.info('Starting freqtrade in Edge mode') logger.info('Starting freqtrade in Edge mode')

View File

@ -7,6 +7,7 @@ from freqtrade.configuration import setup_utils_configuration
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver
from freqtrade.state import RunMode from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -1,5 +1,4 @@
import logging import logging
from typing import Any, Dict from typing import Any, Dict

View File

@ -1,7 +1,7 @@
# flake8: noqa: F401 # flake8: noqa: F401
from freqtrade.configuration.config_setup import setup_utils_configuration
from freqtrade.configuration.check_exchange import check_exchange, remove_credentials from freqtrade.configuration.check_exchange import check_exchange, remove_credentials
from freqtrade.configuration.timerange import TimeRange from freqtrade.configuration.config_setup import setup_utils_configuration
from freqtrade.configuration.configuration import Configuration
from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.configuration.config_validation import validate_config_consistency
from freqtrade.configuration.configuration import Configuration
from freqtrade.configuration.timerange import TimeRange

View File

@ -2,11 +2,11 @@ import logging
from typing import Any, Dict from typing import Any, Dict
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason, from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason, is_exchange_bad,
is_exchange_bad, is_exchange_known_ccxt, is_exchange_known_ccxt, is_exchange_officially_supported)
is_exchange_officially_supported)
from freqtrade.state import RunMode from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -1,10 +1,12 @@
import logging import logging
from typing import Any, Dict from typing import Any, Dict
from freqtrade.state import RunMode
from .check_exchange import remove_credentials
from .config_validation import validate_config_consistency from .config_validation import validate_config_consistency
from .configuration import Configuration from .configuration import Configuration
from .check_exchange import remove_credentials
from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -9,6 +9,7 @@ from freqtrade import constants
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.state import RunMode from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -10,14 +10,14 @@ from typing import Any, Callable, Dict, List, Optional
from freqtrade import constants from freqtrade import constants
from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.check_exchange import check_exchange
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
from freqtrade.configuration.directory_operations import (create_datadir, from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir
create_userdata_dir)
from freqtrade.configuration.load_config import load_config_file from freqtrade.configuration.load_config import load_config_file
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.loggers import setup_logging from freqtrade.loggers import setup_logging
from freqtrade.misc import deep_merge_dicts, json_load from freqtrade.misc import deep_merge_dicts, json_load
from freqtrade.state import NON_UTIL_MODES, TRADING_MODES, RunMode from freqtrade.state import NON_UTIL_MODES, TRADING_MODES, RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -263,6 +263,9 @@ class Configuration:
self._args_to_config(config, argname='hyperopt_path', self._args_to_config(config, argname='hyperopt_path',
logstring='Using additional Hyperopt lookup path: {}') logstring='Using additional Hyperopt lookup path: {}')
self._args_to_config(config, argname='hyperoptexportfilename',
logstring='Using hyperopt file: {}')
self._args_to_config(config, argname='epochs', self._args_to_config(config, argname='epochs',
logstring='Parameter --epochs detected ... ' logstring='Parameter --epochs detected ... '
'Will run Hyperopt with for {} epochs ...' 'Will run Hyperopt with for {} epochs ...'
@ -295,9 +298,6 @@ class Configuration:
self._args_to_config(config, argname='hyperopt_min_trades', self._args_to_config(config, argname='hyperopt_min_trades',
logstring='Parameter --min-trades detected: {}') logstring='Parameter --min-trades detected: {}')
self._args_to_config(config, argname='hyperopt_continue',
logstring='Hyperopt continue: {}')
self._args_to_config(config, argname='hyperopt_loss', self._args_to_config(config, argname='hyperopt_loss',
logstring='Using Hyperopt loss class name: {}') logstring='Using Hyperopt loss class name: {}')

View File

@ -3,8 +3,9 @@ import shutil
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from freqtrade.exceptions import OperationalException
from freqtrade.constants import USER_DATA_FILES from freqtrade.constants import USER_DATA_FILES
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -11,6 +11,7 @@ import rapidjson
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -11,7 +11,6 @@ DEFAULT_EXCHANGE = 'bittrex'
PROCESS_THROTTLE_SECS = 5 # sec PROCESS_THROTTLE_SECS = 5 # sec
HYPEROPT_EPOCH = 100 # epochs HYPEROPT_EPOCH = 100 # epochs
RETRY_TIMEOUT = 30 # sec RETRY_TIMEOUT = 30 # sec
DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss'
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite'
UNLIMITED_STAKE_AMOUNT = 'unlimited' UNLIMITED_STAKE_AMOUNT = 'unlimited'
@ -21,6 +20,9 @@ REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
ORDERBOOK_SIDES = ['ask', 'bid'] ORDERBOOK_SIDES = ['ask', 'bid']
ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTYPE_POSSIBILITIES = ['limit', 'market']
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
'AgeFilter', 'PrecisionFilter', 'PriceFilter', 'AgeFilter', 'PrecisionFilter', 'PriceFilter',
'ShuffleFilter', 'SpreadFilter'] 'ShuffleFilter', 'SpreadFilter']

View File

@ -2,17 +2,17 @@
Helpers when analyzing backtest data Helpers when analyzing backtest data
""" """
import logging import logging
from datetime import timezone
from pathlib import Path from pathlib import Path
from typing import Dict, Union, Tuple, Any, Optional from typing import Any, Dict, Optional, Tuple, Union
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from datetime import timezone
from freqtrade import persistence
from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.constants import LAST_BT_RESULT_FN
from freqtrade.misc import json_load from freqtrade.misc import json_load
from freqtrade.persistence import Trade from freqtrade.persistence import Trade, init_db
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,10 +21,11 @@ BT_DATA_COLUMNS = ["pair", "profit_percent", "open_date", "close_date", "index",
"open_rate", "close_rate", "open_at_end", "sell_reason"] "open_rate", "close_rate", "open_at_end", "sell_reason"]
def get_latest_backtest_filename(directory: Union[Path, str]) -> str: def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str:
""" """
Get latest backtest export based on '.last_result.json'. Get latest backtest export based on '.last_result.json'.
:param directory: Directory to search for last result :param directory: Directory to search for last result
:param variant: 'backtest' or 'hyperopt' - the method to return
:return: string containing the filename of the latest backtest result :return: string containing the filename of the latest backtest result
:raises: ValueError in the following cases: :raises: ValueError in the following cases:
* Directory does not exist * Directory does not exist
@ -44,10 +45,57 @@ def get_latest_backtest_filename(directory: Union[Path, str]) -> str:
with filename.open() as file: with filename.open() as file:
data = json_load(file) data = json_load(file)
if 'latest_backtest' not in data: if f'latest_{variant}' not in data:
raise ValueError(f"Invalid '{LAST_BT_RESULT_FN}' format.") raise ValueError(f"Invalid '{LAST_BT_RESULT_FN}' format.")
return data['latest_backtest'] return data[f'latest_{variant}']
def get_latest_backtest_filename(directory: Union[Path, str]) -> str:
"""
Get latest backtest export based on '.last_result.json'.
:param directory: Directory to search for last result
:return: string containing the filename of the latest backtest result
:raises: ValueError in the following cases:
* Directory does not exist
* `directory/.last_result.json` does not exist
* `directory/.last_result.json` has the wrong content
"""
return get_latest_optimize_filename(directory, 'backtest')
def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str:
"""
Get latest hyperopt export based on '.last_result.json'.
:param directory: Directory to search for last result
:return: string containing the filename of the latest hyperopt result
:raises: ValueError in the following cases:
* Directory does not exist
* `directory/.last_result.json` does not exist
* `directory/.last_result.json` has the wrong content
"""
try:
return get_latest_optimize_filename(directory, 'hyperopt')
except ValueError:
# Return default (legacy) pickle filename
return 'hyperopt_results.pickle'
def get_latest_hyperopt_file(directory: Union[Path, str], predef_filename: str = None) -> Path:
"""
Get latest hyperopt export based on '.last_result.json'.
:param directory: Directory to search for last result
:return: string containing the filename of the latest hyperopt result
:raises: ValueError in the following cases:
* Directory does not exist
* `directory/.last_result.json` does not exist
* `directory/.last_result.json` has the wrong content
"""
if isinstance(directory, str):
directory = Path(directory)
if predef_filename:
return directory / predef_filename
return directory / get_latest_hyperopt_filename(directory)
def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]: def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]:
@ -169,7 +217,7 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF
Can also serve as protection to load the correct result. Can also serve as protection to load the correct result.
:return: Dataframe containing Trades :return: Dataframe containing Trades
""" """
persistence.init(db_url, clean_open_orders=False) init_db(db_url, clean_open_orders=False)
columns = ["pair", "open_date", "close_date", "profit", "profit_percent", columns = ["pair", "open_date", "close_date", "profit", "profit_percent",
"open_rate", "close_rate", "amount", "trade_duration", "sell_reason", "open_rate", "close_rate", "amount", "trade_duration", "sell_reason",

View File

@ -10,8 +10,8 @@ from typing import Any, Dict, List
import pandas as pd import pandas as pd
from pandas import DataFrame, to_datetime from pandas import DataFrame, to_datetime
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS
DEFAULT_TRADES_COLUMNS)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -17,6 +17,7 @@ from freqtrade.exceptions import ExchangeError, OperationalException
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.state import RunMode from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -5,10 +5,8 @@ Includes:
* load data for a pair (or a list of pairs) from disk * load data for a pair (or a list of pairs) from disk
* download data from exchange and store to disk * download data from exchange and store to disk
""" """
# flake8: noqa: F401
from .history_utils import (convert_trades_to_ohlcv, # noqa: F401 from .history_utils import (convert_trades_to_ohlcv, get_timerange, load_data, load_pair_history,
get_timerange, load_data, load_pair_history, refresh_backtest_ohlcv_data, refresh_backtest_trades_data, refresh_data,
refresh_backtest_ohlcv_data,
refresh_backtest_trades_data, refresh_data,
validate_backtest_data) validate_backtest_data)
from .idatahandler import get_datahandler # noqa: F401 from .idatahandler import get_datahandler

View File

@ -7,12 +7,12 @@ import pandas as pd
from freqtrade import misc from freqtrade import misc
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS,
DEFAULT_TRADES_COLUMNS,
ListPairsWithTimeframes) ListPairsWithTimeframes)
from .idatahandler import IDataHandler, TradeList from .idatahandler import IDataHandler, TradeList
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -9,15 +9,14 @@ from pandas import DataFrame
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
from freqtrade.data.converter import (clean_ohlcv_dataframe, from freqtrade.data.converter import (clean_ohlcv_dataframe, ohlcv_to_dataframe,
ohlcv_to_dataframe, trades_remove_duplicates, trades_to_ohlcv)
trades_remove_duplicates,
trades_to_ohlcv)
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.misc import format_ms_time from freqtrade.misc import format_ms_time
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -14,10 +14,10 @@ from pandas import DataFrame
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import ListPairsWithTimeframes from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.data.converter import (clean_ohlcv_dataframe, from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe
trades_remove_duplicates, trim_dataframe)
from freqtrade.exchange import timeframe_to_seconds from freqtrade.exchange import timeframe_to_seconds
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Type for trades list # Type for trades list

View File

@ -8,12 +8,12 @@ from pandas import DataFrame, read_json, to_datetime
from freqtrade import misc from freqtrade import misc
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, ListPairsWithTimeframes
ListPairsWithTimeframes)
from freqtrade.data.converter import trades_dict_to_list from freqtrade.data.converter import trades_dict_to_list
from .idatahandler import IDataHandler, TradeList from .idatahandler import IDataHandler, TradeList
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -9,11 +9,12 @@ import utils_find_1st as utf1st
from pandas import DataFrame from pandas import DataFrame
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, DATETIME_PRINT_FORMAT from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT
from freqtrade.exceptions import OperationalException
from freqtrade.data.history import get_timerange, load_data, refresh_data from freqtrade.data.history import get_timerange, load_data, refresh_data
from freqtrade.exceptions import OperationalException
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -309,8 +310,10 @@ class Edge:
# Calculating number of losing trades, average win and average loss # Calculating number of losing trades, average win and average loss
df['nb_loss_trades'] = df['nb_trades'] - df['nb_win_trades'] df['nb_loss_trades'] = df['nb_trades'] - df['nb_win_trades']
df['average_win'] = df['profit_sum'] / df['nb_win_trades'] df['average_win'] = np.where(df['nb_win_trades'] == 0, 0.0,
df['average_loss'] = df['loss_sum'] / df['nb_loss_trades'] df['profit_sum'] / df['nb_win_trades'])
df['average_loss'] = np.where(df['nb_loss_trades'] == 0, 0.0,
df['loss_sum'] / df['nb_loss_trades'])
# Win rate = number of profitable trades / number of trades # Win rate = number of profitable trades / number of trades
df['winrate'] = df['nb_win_trades'] / df['nb_trades'] df['winrate'] = df['nb_win_trades'] / df['nb_trades']

View File

@ -1,19 +1,16 @@
# flake8: noqa: F401 # flake8: noqa: F401
# isort: off
from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS
from freqtrade.exchange.exchange import Exchange from freqtrade.exchange.exchange import Exchange
from freqtrade.exchange.exchange import (get_exchange_bad_reason, # isort: on
is_exchange_bad,
is_exchange_known_ccxt,
is_exchange_officially_supported,
ccxt_exchanges,
available_exchanges)
from freqtrade.exchange.exchange import (timeframe_to_seconds,
timeframe_to_minutes,
timeframe_to_msecs,
timeframe_to_next_date,
timeframe_to_prev_date)
from freqtrade.exchange.exchange import (market_is_active)
from freqtrade.exchange.kraken import Kraken
from freqtrade.exchange.binance import Binance
from freqtrade.exchange.bibox import Bibox from freqtrade.exchange.bibox import Bibox
from freqtrade.exchange.binance import Binance
from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
get_exchange_bad_reason, is_exchange_bad,
is_exchange_known_ccxt, is_exchange_officially_supported,
market_is_active, timeframe_to_minutes, timeframe_to_msecs,
timeframe_to_next_date, timeframe_to_prev_date,
timeframe_to_seconds)
from freqtrade.exchange.ftx import Ftx from freqtrade.exchange.ftx import Ftx
from freqtrade.exchange.kraken import Kraken

View File

@ -4,6 +4,7 @@ from typing import Dict
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -4,12 +4,12 @@ from typing import Dict
import ccxt import ccxt
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
InvalidOrderException, OperationalException, OperationalException, TemporaryError)
TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier from freqtrade.exchange.common import retrier
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -20,20 +20,9 @@ class Binance(Exchange):
"order_time_in_force": ['gtc', 'fok', 'ioc'], "order_time_in_force": ['gtc', 'fok', 'ioc'],
"trades_pagination": "id", "trades_pagination": "id",
"trades_pagination_arg": "fromId", "trades_pagination_arg": "fromId",
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
} }
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
"""
get order book level 2 from exchange
20180619: binance support limits but only on specific range
"""
limit_range = [5, 10, 20, 50, 100, 500, 1000]
# get next-higher step in the limit_range list
limit = min(list(filter(lambda x: limit <= x, limit_range)))
return super().fetch_l2_order_book(pair, limit)
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
""" """
Verify stop_loss against stoploss-order value (limit or price) Verify stop_loss against stoploss-order value (limit or price)

View File

@ -0,0 +1,23 @@
""" Bittrex exchange subclass """
import logging
from typing import Dict
from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__)
class Bittrex(Exchange):
"""
Bittrex exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
Please note that this exchange is not included in the list of exchanges
officially supported by the Freqtrade development team. So some features
may still not work as expected.
"""
_ft_has: Dict = {
"l2_limit_range": [1, 25, 500],
}

View File

@ -3,8 +3,8 @@ import logging
import time import time
from functools import wraps from functools import wraps
from freqtrade.exceptions import (DDosProtection, RetryableOrderError, from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError
TemporaryError)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -13,20 +13,20 @@ from typing import Any, Dict, List, Optional, Tuple
import arrow import arrow
import ccxt import ccxt
import ccxt.async_support as ccxt_async import ccxt.async_support as ccxt_async
from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE,
TRUNCATE, decimal_to_precision) decimal_to_precision)
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import ListPairsWithTimeframes from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
from freqtrade.exceptions import (DDosProtection, ExchangeError, from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
InsufficientFundsError, InvalidOrderException, OperationalException, RetryableOrderError,
InvalidOrderException, OperationalException, TemporaryError)
RetryableOrderError, TemporaryError) from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, retrier,
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, retrier_async)
BAD_EXCHANGES, retrier, retrier_async)
from freqtrade.misc import deep_merge_dicts, safe_value_fallback2 from freqtrade.misc import deep_merge_dicts, safe_value_fallback2
CcxtModuleType = Any CcxtModuleType = Any
@ -53,7 +53,7 @@ class Exchange:
"ohlcv_partial_candle": True, "ohlcv_partial_candle": True,
"trades_pagination": "time", # Possible are "time" or "id" "trades_pagination": "time", # Possible are "time" or "id"
"trades_pagination_arg": "since", "trades_pagination_arg": "since",
"l2_limit_range": None,
} }
_ft_has: Dict = {} _ft_has: Dict = {}
@ -687,6 +687,9 @@ class Exchange:
async def _async_get_historic_ohlcv(self, pair: str, async def _async_get_historic_ohlcv(self, pair: str,
timeframe: str, timeframe: str,
since_ms: int) -> List: since_ms: int) -> List:
"""
Download historic ohlcv
"""
one_call = timeframe_to_msecs(timeframe) * self._ohlcv_candle_limit one_call = timeframe_to_msecs(timeframe) * self._ohlcv_candle_limit
logger.debug( logger.debug(
@ -702,9 +705,14 @@ class Exchange:
# Combine gathered results # Combine gathered results
data: List = [] data: List = []
for p, timeframe, res in results: for res in results:
if isinstance(res, Exception):
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
continue
# Deconstruct tuple if it's not an exception
p, _, new_data = res
if p == pair: if p == pair:
data.extend(res) data.extend(new_data)
# Sort data again after extending the result - above calls return in "async order" # Sort data again after extending the result - above calls return in "async order"
data = sorted(data, key=lambda x: x[0]) data = sorted(data, key=lambda x: x[0])
logger.info("Downloaded data for %s with length %s.", pair, len(data)) logger.info("Downloaded data for %s with length %s.", pair, len(data))
@ -741,9 +749,8 @@ class Exchange:
if isinstance(res, Exception): if isinstance(res, Exception):
logger.warning("Async code raised an exception: %s", res.__class__.__name__) logger.warning("Async code raised an exception: %s", res.__class__.__name__)
continue continue
pair = res[0] # Deconstruct tuple (has 3 elements)
timeframe = res[1] pair, timeframe, ticks = res
ticks = res[2]
# keeping last candle time as last refreshed time of the pair # keeping last candle time as last refreshed time of the pair
if ticks: if ticks:
self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000 self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
@ -1069,6 +1076,16 @@ class Exchange:
return self.fetch_stoploss_order(order_id, pair) return self.fetch_stoploss_order(order_id, pair)
return self.fetch_order(order_id, pair) return self.fetch_order(order_id, pair)
@staticmethod
def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]]):
"""
Get next greater value in the list.
Used by fetch_l2_order_book if the api only supports a limited range
"""
if not limit_range:
return limit
return min([x for x in limit_range if limit <= x] + [max(limit_range)])
@retrier @retrier
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
""" """
@ -1077,9 +1094,10 @@ class Exchange:
Returns a dict in the format Returns a dict in the format
{'asks': [price, volume], 'bids': [price, volume]} {'asks': [price, volume], 'bids': [price, volume]}
""" """
limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range'])
try: try:
return self._api.fetch_l2_order_book(pair, limit) return self._api.fetch_l2_order_book(pair, limit1)
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(
f'Exchange {self._api.name} does not support fetching order book.' f'Exchange {self._api.name} does not support fetching order book.'

View File

@ -4,12 +4,12 @@ from typing import Any, Dict
import ccxt import ccxt
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
InvalidOrderException, OperationalException, OperationalException, TemporaryError)
TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -4,12 +4,12 @@ from typing import Any, Dict
import ccxt import ccxt
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
InvalidOrderException, OperationalException, OperationalException, TemporaryError)
TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier from freqtrade.exchange.common import retrier
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
import copy import copy
import logging import logging
import traceback import traceback
from datetime import datetime from datetime import datetime, timezone
from math import isclose from math import isclose
from threading import Lock from threading import Lock
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
@ -12,17 +12,17 @@ from typing import Any, Dict, List, Optional
import arrow import arrow
from cachetools import TTLCache from cachetools import TTLCache
from freqtrade import __version__, constants, persistence from freqtrade import __version__, constants
from freqtrade.configuration import validate_config_consistency from freqtrade.configuration import validate_config_consistency
from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError) InvalidOrderException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.misc import safe_value_fallback, safe_value_fallback2
from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.pairlist.pairlistmanager import PairListManager
from freqtrade.persistence import Order, Trade from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.rpc import RPCManager, RPCMessageType
from freqtrade.state import State from freqtrade.state import State
@ -30,6 +30,7 @@ from freqtrade.strategy.interface import IStrategy, SellType
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -57,8 +58,8 @@ class FreqtradeBot:
# Cache values for 1800 to avoid frequent polling of the exchange for prices # Cache values for 1800 to avoid frequent polling of the exchange for prices
# Caching only applies to RPC methods, so prices for open trades are still # Caching only applies to RPC methods, so prices for open trades are still
# refreshed once every iteration. # refreshed once every iteration.
self._sell_rate_cache = TTLCache(maxsize=100, ttl=1800) self._sell_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
self._buy_rate_cache = TTLCache(maxsize=100, ttl=1800) self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
@ -67,10 +68,12 @@ class FreqtradeBot:
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
persistence.init(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run'])
self.wallets = Wallets(self.config, self.exchange) self.wallets = Wallets(self.config, self.exchange)
PairLocks.timeframe = self.config['timeframe']
self.pairlists = PairListManager(self.exchange, self.config) self.pairlists = PairListManager(self.exchange, self.config)
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists)
@ -122,7 +125,7 @@ class FreqtradeBot:
self.check_for_open_trades() self.check_for_open_trades()
self.rpc.cleanup() self.rpc.cleanup()
persistence.cleanup() cleanup_db()
def startup(self) -> None: def startup(self) -> None:
""" """
@ -344,7 +347,7 @@ class FreqtradeBot:
whitelist = copy.deepcopy(self.active_pair_whitelist) whitelist = copy.deepcopy(self.active_pair_whitelist)
if not whitelist: if not whitelist:
logger.info("Active pair whitelist is empty.") logger.info("Active pair whitelist is empty.")
else: return trades_created
# Remove pairs for currently opened trades from the whitelist # Remove pairs for currently opened trades from the whitelist
for trade in Trade.get_open_trades(): for trade in Trade.get_open_trades():
if trade.pair in whitelist: if trade.pair in whitelist:
@ -354,7 +357,7 @@ class FreqtradeBot:
if not whitelist: if not whitelist:
logger.info("No currency pair in active pair whitelist, " logger.info("No currency pair in active pair whitelist, "
"but checking to sell open trades.") "but checking to sell open trades.")
else: return trades_created
# Create entity and execute trade for each pair from whitelist # Create entity and execute trade for each pair from whitelist
for pair in whitelist: for pair in whitelist:
try: try:
@ -936,8 +939,8 @@ class FreqtradeBot:
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
stoploss_order=True) stoploss_order=True)
# Lock pair for one candle to prevent immediate rebuys # Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair, self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
timeframe_to_next_date(self.config['timeframe'])) reason='Auto lock')
self._notify_sell(trade, "stoploss") self._notify_sell(trade, "stoploss")
return True return True
@ -1263,7 +1266,8 @@ class FreqtradeBot:
Trade.session.flush() Trade.session.flush()
# Lock pair for one candle to prevent immediate rebuys # Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe'])) self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
reason='Auto lock')
self._notify_sell(trade, order_type) self._notify_sell(trade, order_type)

View File

@ -1,12 +1,12 @@
import logging import logging
import sys import sys
from logging import Formatter from logging import Formatter
from logging.handlers import (BufferingHandler, RotatingFileHandler, from logging.handlers import BufferingHandler, RotatingFileHandler, SysLogHandler
SysLogHandler)
from typing import Any, Dict from typing import Any, Dict
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
LOGFORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' LOGFORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'

View File

@ -7,6 +7,7 @@ import logging
import sys import sys
from typing import Any, List from typing import Any, List
# check min. python version # check min. python version
if sys.version_info < (3, 6): if sys.version_info < (3, 6):
sys.exit("Freqtrade requires Python version >= 3.6") sys.exit("Freqtrade requires Python version >= 3.6")

View File

@ -12,6 +12,7 @@ from typing.io import IO
import numpy as np import numpy as np
import rapidjson import rapidjson
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -41,7 +42,7 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray:
return dates.dt.to_pydatetime() return dates.dt.to_pydatetime()
def file_dump_json(filename: Path, data: Any, is_zip: bool = False) -> None: def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = True) -> None:
""" """
Dump JSON data into a file Dump JSON data into a file
:param filename: file to create :param filename: file to create
@ -52,11 +53,13 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False) -> None:
if is_zip: if is_zip:
if filename.suffix != '.gz': if filename.suffix != '.gz':
filename = filename.with_suffix('.gz') filename = filename.with_suffix('.gz')
if log:
logger.info(f'dumping json to "{filename}"') logger.info(f'dumping json to "{filename}"')
with gzip.open(filename, 'w') as fp: with gzip.open(filename, 'w') as fpz:
rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE) rapidjson.dump(data, fpz, default=str, number_mode=rapidjson.NM_NATIVE)
else: else:
if log:
logger.info(f'dumping json to "{filename}"') logger.info(f'dumping json to "{filename}"')
with open(filename, 'w') as fp: with open(filename, 'w') as fp:
rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE) rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE)

View File

@ -4,31 +4,39 @@
This module contains the backtesting logic This module contains the backtesting logic
""" """
import logging import logging
from collections import defaultdict
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Dict, List, NamedTuple, Optional, Tuple from typing import Any, Dict, List, NamedTuple, Optional, Tuple
import arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade.configuration import (TimeRange, remove_credentials, from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency
validate_config_consistency)
from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.converter import trim_dataframe from freqtrade.data.converter import trim_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
show_backtest_results,
store_backtest_stats) store_backtest_stats)
from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.pairlist.pairlistmanager import PairListManager
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Indexes for backtest tuples
DATE_IDX = 0
BUY_IDX = 1
OPEN_IDX = 2
CLOSE_IDX = 3
SELL_IDX = 4
LOW_IDX = 5
HIGH_IDX = 6
class BacktestResult(NamedTuple): class BacktestResult(NamedTuple):
""" """
@ -116,7 +124,7 @@ class Backtesting:
""" """
Load strategy into backtesting Load strategy into backtesting
""" """
self.strategy = strategy self.strategy: IStrategy = strategy
# Set stoploss_on_exchange to false for backtesting, # Set stoploss_on_exchange to false for backtesting,
# since a "perfect" stoploss-sell is assumed anyway # since a "perfect" stoploss-sell is assumed anyway
# And the regular "stoploss" function would not apply to that case # And the regular "stoploss" function would not apply to that case
@ -148,12 +156,14 @@ class Backtesting:
return data, timerange return data, timerange
def _get_ohlcv_as_lists(self, processed: Dict) -> Dict[str, DataFrame]: def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]:
""" """
Helper function to convert a processed dataframes into lists for performance reasons. Helper function to convert a processed dataframes into lists for performance reasons.
Used by backtest() - so keep this optimized for performance. Used by backtest() - so keep this optimized for performance.
""" """
# Every change to this headers list must evaluate further usages of the resulting tuple
# and eventually change the constants for indexes at the top
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
data: Dict = {} data: Dict = {}
# Create dict with data # Create dict with data
@ -173,10 +183,10 @@ class Backtesting:
# Convert from Pandas to list for performance reasons # Convert from Pandas to list for performance reasons
# (Looping Pandas is slow.) # (Looping Pandas is slow.)
data[pair] = [x for x in df_analyzed.itertuples()] data[pair] = [x for x in df_analyzed.itertuples(index=False, name=None)]
return data return data
def _get_close_rate(self, sell_row, trade: Trade, sell: SellCheckTuple, def _get_close_rate(self, sell_row: Tuple, trade: Trade, sell: SellCheckTuple,
trade_dur: int) -> float: trade_dur: int) -> float:
""" """
Get close rate for backtesting result Get close rate for backtesting result
@ -187,12 +197,12 @@ class Backtesting:
return trade.stop_loss return trade.stop_loss
elif sell.sell_type == (SellType.ROI): elif sell.sell_type == (SellType.ROI):
roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur) roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur)
if roi is not None: if roi is not None and roi_entry is not None:
if roi == -1 and roi_entry % self.timeframe_min == 0: if roi == -1 and roi_entry % self.timeframe_min == 0:
# When forceselling with ROI=-1, the roi time will always be equal to trade_dur. # When forceselling with ROI=-1, the roi time will always be equal to trade_dur.
# If that entry is a multiple of the timeframe (so on candle open) # If that entry is a multiple of the timeframe (so on candle open)
# - we'll use open instead of close # - we'll use open instead of close
return sell_row.open return sell_row[OPEN_IDX]
# - (Expected abs profit + open_rate + open_fee) / (fee_close -1) # - (Expected abs profit + open_rate + open_fee) / (fee_close -1)
close_rate = - (trade.open_rate * roi + trade.open_rate * close_rate = - (trade.open_rate * roi + trade.open_rate *
@ -200,57 +210,38 @@ class Backtesting:
if (trade_dur > 0 and trade_dur == roi_entry if (trade_dur > 0 and trade_dur == roi_entry
and roi_entry % self.timeframe_min == 0 and roi_entry % self.timeframe_min == 0
and sell_row.open > close_rate): and sell_row[OPEN_IDX] > close_rate):
# new ROI entry came into effect. # new ROI entry came into effect.
# use Open rate if open_rate > calculated sell rate # use Open rate if open_rate > calculated sell rate
return sell_row.open return sell_row[OPEN_IDX]
# Use the maximum between close_rate and low as we # Use the maximum between close_rate and low as we
# cannot sell outside of a candle. # cannot sell outside of a candle.
# Applies when a new ROI setting comes in place and the whole candle is above that. # Applies when a new ROI setting comes in place and the whole candle is above that.
return max(close_rate, sell_row.low) return max(close_rate, sell_row[LOW_IDX])
else: else:
# This should not be reached... # This should not be reached...
return sell_row.open return sell_row[OPEN_IDX]
else: else:
return sell_row.open return sell_row[OPEN_IDX]
def _get_sell_trade_entry( def _get_sell_trade_entry(self, trade: Trade, sell_row: Tuple) -> Optional[BacktestResult]:
self, pair: str, buy_row: DataFrame,
partial_ohlcv: List, trade_count_lock: Dict,
stake_amount: float, max_open_trades: int) -> Optional[BacktestResult]:
trade = Trade( sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], sell_row[DATE_IDX],
pair=pair, sell_row[BUY_IDX], sell_row[SELL_IDX],
open_rate=buy_row.open, low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
open_date=buy_row.date,
stake_amount=stake_amount,
amount=round(stake_amount / buy_row.open, 8),
fee_open=self.fee,
fee_close=self.fee,
is_open=True,
)
logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.")
# calculate win/lose forwards from buy point
for sell_row in partial_ohlcv:
if max_open_trades > 0:
# Increase trade_count_lock for every iteration
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy,
sell_row.sell, low=sell_row.low, high=sell_row.high)
if sell.sell_flag: if sell.sell_flag:
trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60) trade_dur = int((sell_row[DATE_IDX] - trade.open_date).total_seconds() // 60)
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
return BacktestResult(pair=pair, return BacktestResult(pair=trade.pair,
profit_percent=trade.calc_profit_ratio(rate=closerate), profit_percent=trade.calc_profit_ratio(rate=closerate),
profit_abs=trade.calc_profit(rate=closerate), profit_abs=trade.calc_profit(rate=closerate),
open_date=buy_row.date, open_date=trade.open_date,
open_rate=buy_row.open, open_rate=trade.open_rate,
open_fee=self.fee, open_fee=self.fee,
close_date=sell_row.date, close_date=sell_row[DATE_IDX],
close_rate=closerate, close_rate=closerate,
close_fee=self.fee, close_fee=self.fee,
amount=trade.amount, amount=trade.amount,
@ -258,33 +249,40 @@ class Backtesting:
open_at_end=False, open_at_end=False,
sell_reason=sell.sell_type sell_reason=sell.sell_type
) )
if partial_ohlcv: return None
# no sell condition found - trade stil open at end of backtest period
sell_row = partial_ohlcv[-1] def handle_left_open(self, open_trades: Dict[str, List[Trade]],
bt_res = BacktestResult(pair=pair, data: Dict[str, List[Tuple]]) -> List[BacktestResult]:
profit_percent=trade.calc_profit_ratio(rate=sell_row.open), """
profit_abs=trade.calc_profit(rate=sell_row.open), Handling of left open trades at the end of backtesting
open_date=buy_row.date, """
open_rate=buy_row.open, trades = []
for pair in open_trades.keys():
if len(open_trades[pair]) > 0:
for trade in open_trades[pair]:
sell_row = data[pair][-1]
trade_entry = BacktestResult(pair=trade.pair,
profit_percent=trade.calc_profit_ratio(
rate=sell_row[OPEN_IDX]),
profit_abs=trade.calc_profit(sell_row[OPEN_IDX]),
open_date=trade.open_date,
open_rate=trade.open_rate,
open_fee=self.fee, open_fee=self.fee,
close_date=sell_row.date, close_date=sell_row[DATE_IDX],
close_rate=sell_row.open, close_rate=sell_row[OPEN_IDX],
close_fee=self.fee, close_fee=self.fee,
amount=trade.amount, amount=trade.amount,
trade_duration=int(( trade_duration=int((
sell_row.date - buy_row.date).total_seconds() // 60), sell_row[DATE_IDX] - trade.open_date
).total_seconds() // 60),
open_at_end=True, open_at_end=True,
sell_reason=SellType.FORCE_SELL sell_reason=SellType.FORCE_SELL
) )
logger.debug(f"{pair} - Force selling still open trade, " trades.append(trade_entry)
f"profit percent: {bt_res.profit_percent}, " return trades
f"profit abs: {bt_res.profit_abs}")
return bt_res
return None
def backtest(self, processed: Dict, stake_amount: float, def backtest(self, processed: Dict, stake_amount: float,
start_date: arrow.Arrow, end_date: arrow.Arrow, start_date: datetime, end_date: datetime,
max_open_trades: int = 0, position_stacking: bool = False) -> DataFrame: max_open_trades: int = 0, position_stacking: bool = False) -> DataFrame:
""" """
Implement backtesting functionality Implement backtesting functionality
@ -306,19 +304,21 @@ class Backtesting:
f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}" f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}"
) )
trades = [] trades = []
trade_count_lock: Dict = {}
# Use dict of lists with data for performance # Use dict of lists with data for performance
# (looping lists is a lot faster than pandas DataFrames) # (looping lists is a lot faster than pandas DataFrames)
data: Dict = self._get_ohlcv_as_lists(processed) data: Dict = self._get_ohlcv_as_lists(processed)
lock_pair_until: Dict = {}
# Indexes per pair, so some pairs are allowed to have a missing start. # Indexes per pair, so some pairs are allowed to have a missing start.
indexes: Dict = {} indexes: Dict = {}
tmp = start_date + timedelta(minutes=self.timeframe_min) tmp = start_date + timedelta(minutes=self.timeframe_min)
open_trades: Dict[str, List] = defaultdict(list)
open_trade_count = 0
# Loop timerange and get candle for each pair at that point in time # Loop timerange and get candle for each pair at that point in time
while tmp < end_date: while tmp <= end_date:
open_trade_count_start = open_trade_count
for i, pair in enumerate(data): for i, pair in enumerate(data):
if pair not in indexes: if pair not in indexes:
@ -332,42 +332,52 @@ class Backtesting:
continue continue
# Waits until the time-counter reaches the start of the data for this pair. # Waits until the time-counter reaches the start of the data for this pair.
if row.date > tmp.datetime: if row[DATE_IDX] > tmp:
continue continue
indexes[pair] += 1 indexes[pair] += 1
if row.buy == 0 or row.sell == 1:
continue # skip rows where no buy signal or that would immediately sell off
if (not position_stacking and pair in lock_pair_until
and row.date <= lock_pair_until[pair]):
# without positionstacking, we can only have one open trade per pair. # without positionstacking, we can only have one open trade per pair.
continue # max_open_trades must be respected
# don't open on the last row
if max_open_trades > 0: if ((position_stacking or len(open_trades[pair]) == 0)
# Check if max_open_trades has already been reached for the given date and max_open_trades > 0 and open_trade_count_start < max_open_trades
if not trade_count_lock.get(row.date, 0) < max_open_trades: and tmp != end_date
continue and row[BUY_IDX] == 1 and row[SELL_IDX] != 1):
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 # Enter trade
trade = Trade(
pair=pair,
open_rate=row[OPEN_IDX],
open_date=row[DATE_IDX],
stake_amount=stake_amount,
amount=round(stake_amount / row[OPEN_IDX], 8),
fee_open=self.fee,
fee_close=self.fee,
is_open=True,
)
# TODO: hacky workaround to avoid opening > max_open_trades
# This emulates previous behaviour - not sure if this is correct
# Prevents buying if the trade-slot was freed in this candle
open_trade_count_start += 1
open_trade_count += 1
# logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.")
open_trades[pair].append(trade)
for trade in open_trades[pair]:
# since indexes has been incremented before, we need to go one step back to # since indexes has been incremented before, we need to go one step back to
# also check the buying candle for sell conditions. # also check the buying candle for sell conditions.
trade_entry = self._get_sell_trade_entry(pair, row, data[pair][indexes[pair]-1:], trade_entry = self._get_sell_trade_entry(trade, row)
trade_count_lock, stake_amount, # Sell occured
max_open_trades)
if trade_entry: if trade_entry:
logger.debug(f"{pair} - Locking pair till " # logger.debug(f"{pair} - Backtesting sell {trade}")
f"close_date={trade_entry.close_date}") open_trade_count -= 1
lock_pair_until[pair] = trade_entry.close_date open_trades[pair].remove(trade)
trades.append(trade_entry) trades.append(trade_entry)
else:
# Set lock_pair_until to end of testing period if trade could not be closed
lock_pair_until[pair] = end_date.datetime
# Move time one configured time_interval ahead. # Move time one configured time_interval ahead.
tmp += timedelta(minutes=self.timeframe_min) tmp += timedelta(minutes=self.timeframe_min)
trades += self.handle_left_open(open_trades, data=data)
return DataFrame.from_records(trades, columns=BacktestResult._fields) return DataFrame.from_records(trades, columns=BacktestResult._fields)
def start(self) -> None: def start(self) -> None:
@ -413,8 +423,8 @@ class Backtesting:
results = self.backtest( results = self.backtest(
processed=preprocessed, processed=preprocessed,
stake_amount=self.config['stake_amount'], stake_amount=self.config['stake_amount'],
start_date=min_date, start_date=min_date.datetime,
end_date=max_date, end_date=max_date.datetime,
max_open_trades=max_open_trades, max_open_trades=max_open_trades,
position_stacking=position_stacking, position_stacking=position_stacking,
) )

View File

@ -1,5 +1,5 @@
""" """
DefaultHyperOptLoss ShortTradeDurHyperOptLoss
This module defines the default HyperoptLoss class which is being used for This module defines the default HyperoptLoss class which is being used for
Hyperoptimization. Hyperoptimization.
""" """
@ -26,7 +26,7 @@ EXPECTED_MAX_PROFIT = 3.0
MAX_ACCEPTED_TRADE_DURATION = 300 MAX_ACCEPTED_TRADE_DURATION = 300
class DefaultHyperOptLoss(IHyperOptLoss): class ShortTradeDurHyperOptLoss(IHyperOptLoss):
""" """
Defines the default loss function for hyperopt Defines the default loss function for hyperopt
""" """
@ -50,3 +50,7 @@ class DefaultHyperOptLoss(IHyperOptLoss):
duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1)
result = trade_loss + profit_loss + duration_loss result = trade_loss + profit_loss + duration_loss
return result return result
# Create an alias for This to allow the legacy Method to work as well.
DefaultHyperOptLoss = ShortTradeDurHyperOptLoss

View File

@ -7,12 +7,12 @@ import logging
from typing import Any, Dict from typing import Any, Dict
from freqtrade import constants from freqtrade import constants
from freqtrade.configuration import (TimeRange, remove_credentials, from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency
validate_config_consistency)
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.optimize.optimize_reports import generate_edge_table from freqtrade.optimize.optimize_reports import generate_edge_table
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -10,6 +10,7 @@ import logging
import random import random
import warnings import warnings
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime
from math import ceil from math import ceil
from operator import itemgetter from operator import itemgetter
from pathlib import Path from pathlib import Path
@ -21,24 +22,22 @@ import rapidjson
import tabulate import tabulate
from colorama import Fore, Style from colorama import Fore, Style
from colorama import init as colorama_init from colorama import init as colorama_init
from joblib import (Parallel, cpu_count, delayed, dump, load, from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
wrap_non_picklable_objects)
from pandas import DataFrame, isna, json_normalize from pandas import DataFrame, isna, json_normalize
from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
from freqtrade.data.converter import trim_dataframe from freqtrade.data.converter import trim_dataframe
from freqtrade.data.history import get_timerange from freqtrade.data.history import get_timerange
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import plural, round_dict from freqtrade.misc import file_dump_json, plural, round_dict
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
from freqtrade.optimize.hyperopt_loss_interface import \ from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
IHyperOptLoss # noqa: F401 from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver
from freqtrade.resolvers.hyperopt_resolver import (HyperOptLossResolver,
HyperOptResolver)
from freqtrade.strategy import IStrategy from freqtrade.strategy import IStrategy
# Suppress scikit-learn FutureWarnings from skopt # Suppress scikit-learn FutureWarnings from skopt
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=FutureWarning) warnings.filterwarnings("ignore", category=FutureWarning)
@ -77,19 +76,16 @@ class Hyperopt:
self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config) self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config)
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
self.results_file = (self.config['user_data_dir'] / self.results_file = (self.config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_results.pickle') 'hyperopt_results' / f'hyperopt_results_{time_now}.pickle')
self.data_pickle_file = (self.config['user_data_dir'] / self.data_pickle_file = (self.config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_tickerdata.pkl') 'hyperopt_results' / 'hyperopt_tickerdata.pkl')
self.total_epochs = config.get('epochs', 0) self.total_epochs = config.get('epochs', 0)
self.current_best_loss = 100 self.current_best_loss = 100
if not self.config.get('hyperopt_continue'):
self.clean_hyperopt() self.clean_hyperopt()
else:
logger.info("Continuing on previous hyperopt results.")
self.num_epochs_saved = 0 self.num_epochs_saved = 0
@ -98,14 +94,14 @@ class Hyperopt:
# Populate functions here (hasattr is slow so should not be run during "regular" operations) # Populate functions here (hasattr is slow so should not be run during "regular" operations)
if hasattr(self.custom_hyperopt, 'populate_indicators'): if hasattr(self.custom_hyperopt, 'populate_indicators'):
self.backtesting.strategy.advise_indicators = \ self.backtesting.strategy.advise_indicators = ( # type: ignore
self.custom_hyperopt.populate_indicators # type: ignore self.custom_hyperopt.populate_indicators) # type: ignore
if hasattr(self.custom_hyperopt, 'populate_buy_trend'): if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
self.backtesting.strategy.advise_buy = \ self.backtesting.strategy.advise_buy = ( # type: ignore
self.custom_hyperopt.populate_buy_trend # type: ignore self.custom_hyperopt.populate_buy_trend) # type: ignore
if hasattr(self.custom_hyperopt, 'populate_sell_trend'): if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
self.backtesting.strategy.advise_sell = \ self.backtesting.strategy.advise_sell = ( # type: ignore
self.custom_hyperopt.populate_sell_trend # type: ignore self.custom_hyperopt.populate_sell_trend) # type: ignore
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True): if self.config.get('use_max_market_positions', True):
@ -165,6 +161,10 @@ class Hyperopt:
self.num_epochs_saved = num_epochs self.num_epochs_saved = num_epochs
logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} " logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
f"saved to '{self.results_file}'.") f"saved to '{self.results_file}'.")
# Store hyperopt filename
latest_filename = Path.joinpath(self.results_file.parent, LAST_BT_RESULT_FN)
file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)},
log=False)
@staticmethod @staticmethod
def _read_results(results_file: Path) -> List: def _read_results(results_file: Path) -> List:
@ -262,6 +262,11 @@ class Hyperopt:
), ),
default=str, indent=4, number_mode=rapidjson.NM_NATIVE) default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
params_result += f"minimal_roi = {minimal_roi_result}" params_result += f"minimal_roi = {minimal_roi_result}"
elif space == 'trailing':
for k, v in space_params.items():
params_result += f'{k} = {v}\n'
else: else:
params_result += f"{space}_params = {pformat(space_params, indent=4)}" params_result += f"{space}_params = {pformat(space_params, indent=4)}"
params_result = params_result.replace("}", "\n}").replace("{", "{\n ") params_result = params_result.replace("}", "\n}").replace("{", "{\n ")
@ -503,16 +508,16 @@ class Hyperopt:
params_details = self._get_params_details(params_dict) params_details = self._get_params_details(params_dict)
if self.has_space('roi'): if self.has_space('roi'):
self.backtesting.strategy.minimal_roi = \ self.backtesting.strategy.minimal_roi = ( # type: ignore
self.custom_hyperopt.generate_roi_table(params_dict) self.custom_hyperopt.generate_roi_table(params_dict))
if self.has_space('buy'): if self.has_space('buy'):
self.backtesting.strategy.advise_buy = \ self.backtesting.strategy.advise_buy = ( # type: ignore
self.custom_hyperopt.buy_strategy_generator(params_dict) self.custom_hyperopt.buy_strategy_generator(params_dict))
if self.has_space('sell'): if self.has_space('sell'):
self.backtesting.strategy.advise_sell = \ self.backtesting.strategy.advise_sell = ( # type: ignore
self.custom_hyperopt.sell_strategy_generator(params_dict) self.custom_hyperopt.sell_strategy_generator(params_dict))
if self.has_space('stoploss'): if self.has_space('stoploss'):
self.backtesting.strategy.stoploss = params_dict['stoploss'] self.backtesting.strategy.stoploss = params_dict['stoploss']
@ -533,8 +538,8 @@ class Hyperopt:
backtesting_results = self.backtesting.backtest( backtesting_results = self.backtesting.backtest(
processed=processed, processed=processed,
stake_amount=self.config['stake_amount'], stake_amount=self.config['stake_amount'],
start_date=min_date, start_date=min_date.datetime,
end_date=max_date, end_date=max_date.datetime,
max_open_trades=self.max_open_trades, max_open_trades=self.max_open_trades,
position_stacking=self.position_stacking, position_stacking=self.position_stacking,
) )
@ -657,8 +662,6 @@ class Hyperopt:
self.backtesting.strategy.dp = None # type: ignore self.backtesting.strategy.dp = None # type: ignore
IStrategy.dp = None # type: ignore IStrategy.dp = None # type: ignore
self.epochs = self.load_previous_results(self.results_file)
cpus = cpu_count() cpus = cpu_count()
logger.info(f"Found {cpus} CPU cores. Let's make them scream!") logger.info(f"Found {cpus} CPU cores. Let's make them scream!")
config_jobs = self.config.get('hyperopt_jobs', -1) config_jobs = self.config.get('hyperopt_jobs', -1)

View File

@ -13,6 +13,7 @@ from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import round_dict from freqtrade.misc import round_dict
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -6,8 +6,8 @@ Hyperoptimization.
""" """
from datetime import datetime from datetime import datetime
from pandas import DataFrame
import numpy as np import numpy as np
from pandas import DataFrame
from freqtrade.optimize.hyperopt import IHyperOptLoss from freqtrade.optimize.hyperopt import IHyperOptLoss

View File

@ -6,8 +6,8 @@ Hyperoptimization.
""" """
from datetime import datetime from datetime import datetime
from pandas import DataFrame
import numpy as np import numpy as np
from pandas import DataFrame
from freqtrade.optimize.hyperopt import IHyperOptLoss from freqtrade.optimize.hyperopt import IHyperOptLoss

View File

@ -4,14 +4,15 @@ from pathlib import Path
from typing import Any, Dict, List, Union from typing import Any, Dict, List, Union
from arrow import Arrow from arrow import Arrow
from pandas import DataFrame
from numpy import int64 from numpy import int64
from pandas import DataFrame
from tabulate import tabulate from tabulate import tabulate
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
from freqtrade.data.btanalysis import calculate_max_drawdown, calculate_market_change from freqtrade.data.btanalysis import calculate_market_change, calculate_max_drawdown
from freqtrade.misc import file_dump_json from freqtrade.misc import file_dump_json
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -2,9 +2,10 @@
Minimum age (days listed) pair list filter Minimum age (days listed) pair list filter
""" """
import logging import logging
import arrow
from typing import Any, Dict from typing import Any, Dict
import arrow
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import plural from freqtrade.misc import plural
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList

View File

@ -36,7 +36,7 @@ class IPairList(ABC):
self._pairlist_pos = pairlist_pos self._pairlist_pos = pairlist_pos
self.refresh_period = self._pairlistconfig.get('refresh_period', 1800) self.refresh_period = self._pairlistconfig.get('refresh_period', 1800)
self._last_refresh = 0 self._last_refresh = 0
self._log_cache = TTLCache(maxsize=1024, ttl=self.refresh_period) self._log_cache: TTLCache = TTLCache(maxsize=1024, ttl=self.refresh_period)
@property @property
def name(self) -> str: def name(self) -> str:

View File

@ -4,8 +4,9 @@ Precision pair list filter
import logging import logging
from typing import Any, Dict from typing import Any, Dict
from freqtrade.pairlist.IPairList import IPairList
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -7,10 +7,10 @@ from typing import Dict, List
from cachetools import TTLCache, cached from cachetools import TTLCache, cached
from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
from freqtrade.resolvers import PairListResolver from freqtrade.resolvers import PairListResolver
from freqtrade.constants import ListPairsWithTimeframes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -1,4 +1,4 @@
# flake8: noqa: F401 # flake8: noqa: F401
from freqtrade.persistence.models import (Order, Trade, clean_dry_run_db, from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup_db, init_db
cleanup, init) from freqtrade.persistence.pairlock_middleware import PairLocks

View File

@ -3,6 +3,7 @@ from typing import List
from sqlalchemy import inspect from sqlalchemy import inspect
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -7,8 +7,8 @@ from decimal import Decimal
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import arrow import arrow
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String,
String, create_engine, desc, func, inspect) create_engine, desc, func, inspect)
from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.exc import NoSuchModuleError
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Query, relationship from sqlalchemy.orm import Query, relationship
@ -17,10 +17,12 @@ from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.pool import StaticPool from sqlalchemy.pool import StaticPool
from sqlalchemy.sql.schema import UniqueConstraint from sqlalchemy.sql.schema import UniqueConstraint
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.misc import safe_value_fallback from freqtrade.misc import safe_value_fallback
from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.migrations import check_migrate
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -28,7 +30,7 @@ _DECL_BASE: Any = declarative_base()
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
def init(db_url: str, clean_open_orders: bool = False) -> None: def init_db(db_url: str, clean_open_orders: bool = False) -> 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
@ -62,6 +64,9 @@ def init(db_url: str, clean_open_orders: bool = False) -> None:
# Copy session attributes to order object too # Copy session attributes to order object too
Order.session = Trade.session Order.session = Trade.session
Order.query = Order.session.query_property() Order.query = Order.session.query_property()
PairLock.session = Trade.session
PairLock.query = PairLock.session.query_property()
previous_tables = inspect(engine).get_table_names() previous_tables = inspect(engine).get_table_names()
_DECL_BASE.metadata.create_all(engine) _DECL_BASE.metadata.create_all(engine)
check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables) check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables)
@ -71,7 +76,7 @@ def init(db_url: str, clean_open_orders: bool = False) -> None:
clean_dry_run_db() clean_dry_run_db()
def cleanup() -> None: def cleanup_db() -> None:
""" """
Flushes all pending operations to disk. Flushes all pending operations to disk.
:return: None :return: None
@ -166,12 +171,12 @@ class Order(_DECL_BASE):
""" """
Get all non-closed orders - useful when trying to batch-update orders Get all non-closed orders - useful when trying to batch-update orders
""" """
filtered_orders = [o for o in orders if o.order_id == order['id']] filtered_orders = [o for o in orders if o.order_id == order.get('id')]
if filtered_orders: if filtered_orders:
oobj = filtered_orders[0] oobj = filtered_orders[0]
oobj.update_from_ccxt_object(order) oobj.update_from_ccxt_object(order)
else: else:
logger.warning(f"Did not find order for {order['id']}.") logger.warning(f"Did not find order for {order}.")
@staticmethod @staticmethod
def parse_from_ccxt_object(order: Dict[str, Any], pair: str, side: str) -> 'Order': def parse_from_ccxt_object(order: Dict[str, Any], pair: str, side: str) -> 'Order':
@ -250,7 +255,7 @@ class Trade(_DECL_BASE):
self.recalc_open_trade_price() self.recalc_open_trade_price()
def __repr__(self): def __repr__(self):
open_since = self.open_date.strftime('%Y-%m-%d %H:%M:%S') if self.is_open else 'closed' open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
f'open_rate={self.open_rate:.8f}, open_since={open_since})') f'open_rate={self.open_rate:.8f}, open_since={open_since})')
@ -276,7 +281,7 @@ class Trade(_DECL_BASE):
'fee_close_currency': self.fee_close_currency, 'fee_close_currency': self.fee_close_currency,
'open_date_hum': arrow.get(self.open_date).humanize(), 'open_date_hum': arrow.get(self.open_date).humanize(),
'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"), 'open_date': self.open_date.strftime(DATETIME_PRINT_FORMAT),
'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000), 'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000),
'open_rate': self.open_rate, 'open_rate': self.open_rate,
'open_rate_requested': self.open_rate_requested, 'open_rate_requested': self.open_rate_requested,
@ -284,7 +289,7 @@ class Trade(_DECL_BASE):
'close_date_hum': (arrow.get(self.close_date).humanize() 'close_date_hum': (arrow.get(self.close_date).humanize()
if self.close_date else None), if self.close_date else None),
'close_date': (self.close_date.strftime("%Y-%m-%d %H:%M:%S") 'close_date': (self.close_date.strftime(DATETIME_PRINT_FORMAT)
if self.close_date else None), if self.close_date else None),
'close_timestamp': int(self.close_date.replace( 'close_timestamp': int(self.close_date.replace(
tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None, tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None,
@ -300,7 +305,7 @@ class Trade(_DECL_BASE):
'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
'stoploss_order_id': self.stoploss_order_id, 'stoploss_order_id': self.stoploss_order_id,
'stoploss_last_update': (self.stoploss_last_update.strftime("%Y-%m-%d %H:%M:%S") 'stoploss_last_update': (self.stoploss_last_update.strftime(DATETIME_PRINT_FORMAT)
if self.stoploss_last_update else None), if self.stoploss_last_update else None),
'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace( 'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace(
tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None, tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None,
@ -398,7 +403,7 @@ class Trade(_DECL_BASE):
self.close(order['average']) self.close(order['average'])
else: else:
raise ValueError(f'Unknown order type: {order_type}') raise ValueError(f'Unknown order type: {order_type}')
cleanup() cleanup_db()
def close(self, rate: float) -> None: def close(self, rate: float) -> None:
""" """
@ -653,3 +658,56 @@ class Trade(_DECL_BASE):
trade.stop_loss = None trade.stop_loss = None
trade.adjust_stop_loss(trade.open_rate, desired_stoploss) trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
logger.info(f"New stoploss: {trade.stop_loss}.") logger.info(f"New stoploss: {trade.stop_loss}.")
class PairLock(_DECL_BASE):
"""
Pair Locks database model.
"""
__tablename__ = 'pairlocks'
id = Column(Integer, primary_key=True)
pair = Column(String, nullable=False, index=True)
reason = Column(String, nullable=True)
# Time the pair was locked (start time)
lock_time = Column(DateTime, nullable=False)
# Time until the pair is locked (end time)
lock_end_time = Column(DateTime, nullable=False, index=True)
active = Column(Boolean, nullable=False, default=True, index=True)
def __repr__(self):
lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT)
lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT)
return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, '
f'lock_end_time={lock_end_time})')
@staticmethod
def query_pair_locks(pair: Optional[str], now: datetime) -> Query:
"""
Get all locks for this pair
:param pair: Pair to check for. Returns all current locks if pair is empty
:param now: Datetime object (generated via datetime.now(timezone.utc)).
"""
filters = [PairLock.lock_end_time > now,
# Only active locks
PairLock.active.is_(True), ]
if pair:
filters.append(PairLock.pair == pair)
return PairLock.query.filter(
*filters
)
def to_json(self) -> Dict[str, Any]:
return {
'pair': self.pair,
'lock_time': self.lock_time.strftime(DATETIME_PRINT_FORMAT),
'lock_timestamp': int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000),
'lock_end_time': self.lock_end_time.strftime(DATETIME_PRINT_FORMAT),
'lock_end_timestamp': int(self.lock_end_time.replace(tzinfo=timezone.utc
).timestamp() * 1000),
'reason': self.reason,
'active': self.active,
}

View File

@ -0,0 +1,99 @@
import logging
from datetime import datetime, timezone
from typing import List, Optional
from freqtrade.exchange import timeframe_to_next_date
from freqtrade.persistence.models import PairLock
logger = logging.getLogger(__name__)
class PairLocks():
"""
Pairlocks middleware class
Abstracts the database layer away so it becomes optional - which will be necessary to support
backtesting and hyperopt in the future.
"""
use_db = True
locks: List[PairLock] = []
timeframe: str = ''
@staticmethod
def lock_pair(pair: str, until: datetime, reason: str = None) -> None:
lock = PairLock(
pair=pair,
lock_time=datetime.now(timezone.utc),
lock_end_time=timeframe_to_next_date(PairLocks.timeframe, until),
reason=reason,
active=True
)
if PairLocks.use_db:
PairLock.session.add(lock)
PairLock.session.flush()
else:
PairLocks.locks.append(lock)
@staticmethod
def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List[PairLock]:
"""
Get all currently active locks for this pair
:param pair: Pair to check for. Returns all current locks if pair is empty
:param now: Datetime object (generated via datetime.now(timezone.utc)).
defaults to datetime.now(timezone.utc)
"""
if not now:
now = datetime.now(timezone.utc)
if PairLocks.use_db:
return PairLock.query_pair_locks(pair, now).all()
else:
locks = [lock for lock in PairLocks.locks if (
lock.lock_end_time >= now
and lock.active is True
and (pair is None or lock.pair == pair)
)]
return locks
@staticmethod
def unlock_pair(pair: str, now: Optional[datetime] = None) -> None:
"""
Release all locks for this pair.
:param pair: Pair to unlock
:param now: Datetime object (generated via datetime.now(timezone.utc)).
defaults to datetime.now(timezone.utc)
"""
if not now:
now = datetime.now(timezone.utc)
logger.info(f"Releasing all locks for {pair}.")
locks = PairLocks.get_pair_locks(pair, now)
for lock in locks:
lock.active = False
if PairLocks.use_db:
PairLock.session.flush()
@staticmethod
def is_global_lock(now: Optional[datetime] = None) -> bool:
"""
:param now: Datetime object (generated via datetime.now(timezone.utc)).
defaults to datetime.now(timezone.utc)
"""
if not now:
now = datetime.now(timezone.utc)
return len(PairLocks.get_pair_locks('*', now)) > 0
@staticmethod
def is_pair_locked(pair: str, now: Optional[datetime] = None) -> bool:
"""
:param pair: Pair to check for
:param now: Datetime object (generated via datetime.now(timezone.utc)).
defaults to datetime.now(timezone.utc)
"""
if not now:
now = datetime.now(timezone.utc)
return len(PairLocks.get_pair_locks(pair, now)) > 0 or PairLocks.is_global_lock(now)

View File

@ -5,11 +5,8 @@ from typing import Any, Dict, List
import pandas as pd import pandas as pd
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.btanalysis import (calculate_max_drawdown, from freqtrade.data.btanalysis import (calculate_max_drawdown, combine_dataframes_with_mean,
combine_dataframes_with_mean, create_cum_profit, extract_trades_of_period, load_trades)
create_cum_profit,
extract_trades_of_period,
load_trades)
from freqtrade.data.converter import trim_dataframe from freqtrade.data.converter import trim_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.data.history import load_data from freqtrade.data.history import load_data
@ -19,13 +16,14 @@ from freqtrade.misc import pair_to_filename
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy import IStrategy from freqtrade.strategy import IStrategy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
try: try:
from plotly.subplots import make_subplots
from plotly.offline import plot
import plotly.graph_objects as go import plotly.graph_objects as go
from plotly.offline import plot
from plotly.subplots import make_subplots
except ImportError: except ImportError:
logger.exception("Module plotly not found \n Please install using `pip3 install plotly`") logger.exception("Module plotly not found \n Please install using `pip3 install plotly`")
exit(1) exit(1)

View File

@ -1,6 +1,12 @@
from freqtrade.resolvers.iresolver import IResolver # noqa: F401 # flake8: noqa: F401
from freqtrade.resolvers.exchange_resolver import ExchangeResolver # noqa: F401 # isort: off
from freqtrade.resolvers.iresolver import IResolver
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
# isort: on
# Don't import HyperoptResolver to avoid loading the whole Optimize tree # Don't import HyperoptResolver to avoid loading the whole Optimize tree
# from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver # noqa: F401 # from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
from freqtrade.resolvers.pairlist_resolver import PairListResolver # noqa: F401 from freqtrade.resolvers.pairlist_resolver import PairListResolver
from freqtrade.resolvers.strategy_resolver import StrategyResolver # noqa: F401 from freqtrade.resolvers.strategy_resolver import StrategyResolver

View File

@ -3,10 +3,11 @@ This module loads custom exchanges
""" """
import logging import logging
from freqtrade.exchange import Exchange, MAP_EXCHANGE_CHILDCLASS
import freqtrade.exchange as exchanges import freqtrade.exchange as exchanges
from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, Exchange
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -7,12 +7,13 @@ import logging
from pathlib import Path from pathlib import Path
from typing import Dict from typing import Dict
from freqtrade.constants import DEFAULT_HYPEROPT_LOSS, USERPATH_HYPEROPTS from freqtrade.constants import HYPEROPT_LOSS_BUILTIN, USERPATH_HYPEROPTS
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_interface import IHyperOpt
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -69,10 +70,13 @@ class HyperOptLossResolver(IResolver):
:param config: configuration dictionary :param config: configuration dictionary
""" """
# Verify the hyperopt_loss is in the configuration, otherwise fallback to the hyperoptloss_name = config.get('hyperopt_loss')
# default hyperopt loss if not hyperoptloss_name:
hyperoptloss_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS raise OperationalException(
"No Hyperopt loss set. Please use `--hyperopt-loss` to "
"specify the Hyperopt-Loss class to use.\n"
f"Built-in Hyperopt-loss-functions are: {', '.join(HYPEROPT_LOSS_BUILTIN)}"
)
hyperoptloss = HyperOptLossResolver.load_object(hyperoptloss_name, hyperoptloss = HyperOptLossResolver.load_object(hyperoptloss_name,
config, kwargs={}, config, kwargs={},
extra_dir=config.get('hyperopt_path')) extra_dir=config.get('hyperopt_path'))
@ -81,8 +85,4 @@ class HyperOptLossResolver(IResolver):
hyperoptloss.__class__.ticker_interval = str(config['timeframe']) hyperoptloss.__class__.ticker_interval = str(config['timeframe'])
hyperoptloss.__class__.timeframe = str(config['timeframe']) hyperoptloss.__class__.timeframe = str(config['timeframe'])
if not hasattr(hyperoptloss, 'hyperopt_loss_function'):
raise OperationalException(
f"Found HyperoptLoss class {hyperoptloss_name} does not "
"implement `hyperopt_loss_function`.")
return hyperoptloss return hyperoptloss

View File

@ -11,6 +11,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -50,7 +51,8 @@ class IResolver:
:param object_name: Class name of the object :param object_name: Class name of the object
:param enum_failed: If True, will return None for modules which fail. :param enum_failed: If True, will return None for modules which fail.
Otherwise, failing modules are skipped. Otherwise, failing modules are skipped.
:return: generator containing matching objects :return: generator containing tuple of matching objects
Tuple format: [Object, source]
""" """
# Generate spec based on absolute path # Generate spec based on absolute path
@ -66,14 +68,16 @@ class IResolver:
return iter([None]) return iter([None])
valid_objects_gen = ( valid_objects_gen = (
obj for name, obj in inspect.getmembers(module, inspect.isclass) (obj, inspect.getsource(module)) for
if ((object_name is None or object_name == name) and name, obj in inspect.getmembers(
issubclass(obj, cls.object_type) and obj is not cls.object_type) module, inspect.isclass) if ((object_name is None or object_name == name)
and issubclass(obj, cls.object_type)
and obj is not cls.object_type)
) )
return valid_objects_gen return valid_objects_gen
@classmethod @classmethod
def _search_object(cls, directory: Path, object_name: str def _search_object(cls, directory: Path, *, object_name: str, add_source: bool = False
) -> Union[Tuple[Any, Path], Tuple[None, None]]: ) -> Union[Tuple[Any, Path], Tuple[None, None]]:
""" """
Search for the objectname in the given directory Search for the objectname in the given directory
@ -92,11 +96,14 @@ class IResolver:
obj = next(cls._get_valid_object(module_path, object_name), None) obj = next(cls._get_valid_object(module_path, object_name), None)
if obj: if obj:
return (obj, module_path) obj[0].__file__ = str(entry)
if add_source:
obj[0].__source__ = obj[1]
return (obj[0], module_path)
return (None, None) return (None, None)
@classmethod @classmethod
def _load_object(cls, paths: List[Path], object_name: str, def _load_object(cls, paths: List[Path], *, object_name: str, add_source: bool = False,
kwargs: dict = {}) -> Optional[Any]: kwargs: dict = {}) -> Optional[Any]:
""" """
Try to load object from path list. Try to load object from path list.
@ -105,7 +112,8 @@ class IResolver:
for _path in paths: for _path in paths:
try: try:
(module, module_path) = cls._search_object(directory=_path, (module, module_path) = cls._search_object(directory=_path,
object_name=object_name) object_name=object_name,
add_source=add_source)
if module: if module:
logger.info( logger.info(
f"Using resolved {cls.object_type.__name__.lower()[1:]} {object_name} " f"Using resolved {cls.object_type.__name__.lower()[1:]} {object_name} "
@ -117,7 +125,7 @@ class IResolver:
return None return None
@classmethod @classmethod
def load_object(cls, object_name: str, config: dict, kwargs: dict, def load_object(cls, object_name: str, config: dict, *, kwargs: dict,
extra_dir: Optional[str] = None) -> Any: extra_dir: Optional[str] = None) -> Any:
""" """
Search and loads the specified object as configured in hte child class. Search and loads the specified object as configured in hte child class.
@ -132,10 +140,10 @@ class IResolver:
user_subdir=cls.user_subdir, user_subdir=cls.user_subdir,
extra_dir=extra_dir) extra_dir=extra_dir)
pairlist = cls._load_object(paths=abs_paths, object_name=object_name, found_object = cls._load_object(paths=abs_paths, object_name=object_name,
kwargs=kwargs) kwargs=kwargs)
if pairlist: if found_object:
return pairlist return found_object
raise OperationalException( raise OperationalException(
f"Impossible to load {cls.object_type_str} '{object_name}'. This class does not exist " f"Impossible to load {cls.object_type_str} '{object_name}'. This class does not exist "
"or contains Python code errors." "or contains Python code errors."
@ -163,8 +171,8 @@ class IResolver:
for obj in cls._get_valid_object(module_path, object_name=None, for obj in cls._get_valid_object(module_path, object_name=None,
enum_failed=enum_failed): enum_failed=enum_failed):
objects.append( objects.append(
{'name': obj.__name__ if obj is not None else '', {'name': obj[0].__name__ if obj is not None else '',
'class': obj, 'class': obj[0] if obj is not None else None,
'location': entry, 'location': entry,
}) })
return objects return objects

View File

@ -9,6 +9,7 @@ from pathlib import Path
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -11,12 +11,12 @@ from inspect import getfullargspec
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from freqtrade.constants import (REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, from freqtrade.constants import REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, USERPATH_STRATEGIES
USERPATH_STRATEGIES)
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -174,7 +174,9 @@ class StrategyResolver(IResolver):
strategy = StrategyResolver._load_object(paths=abs_paths, strategy = StrategyResolver._load_object(paths=abs_paths,
object_name=strategy_name, object_name=strategy_name,
kwargs={'config': config}) add_source=True,
kwargs={'config': config},
)
if strategy: if strategy:
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)

View File

@ -1,2 +1,3 @@
from .rpc import RPC, RPCMessageType, RPCException # noqa # flake8: noqa: F401
from .rpc_manager import RPCManager # noqa from .rpc import RPC, RPCException, RPCMessageType
from .rpc_manager import RPCManager

View File

@ -1,40 +1,43 @@
import logging import logging
import threading import threading
from copy import deepcopy
from datetime import date, datetime from datetime import date, datetime
from ipaddress import IPv4Address from ipaddress import IPv4Address
from pathlib import Path
from typing import Any, Callable, Dict from typing import Any, Callable, Dict
from arrow import Arrow from arrow import Arrow
from flask import Flask, jsonify, request from flask import Flask, jsonify, request
from flask.json import JSONEncoder from flask.json import JSONEncoder
from flask_cors import CORS from flask_cors import CORS
from flask_jwt_extended import (JWTManager, create_access_token, from flask_jwt_extended import (JWTManager, create_access_token, create_refresh_token,
create_refresh_token, get_jwt_identity, get_jwt_identity, jwt_refresh_token_required,
jwt_refresh_token_required,
verify_jwt_in_request_optional) verify_jwt_in_request_optional)
from werkzeug.security import safe_str_cmp from werkzeug.security import safe_str_cmp
from werkzeug.serving import make_server from werkzeug.serving import make_server
from freqtrade.__init__ import __version__ from freqtrade.__init__ import __version__
from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.constants import DATETIME_PRINT_FORMAT, USERPATH_STRATEGIES
from freqtrade.exceptions import OperationalException
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.rpc.rpc import RPC, RPCException from freqtrade.rpc.rpc import RPC, RPCException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
BASE_URI = "/api/v1" BASE_URI = "/api/v1"
class ArrowJSONEncoder(JSONEncoder): class FTJSONEncoder(JSONEncoder):
def default(self, obj): def default(self, obj):
try: try:
if isinstance(obj, Arrow): if isinstance(obj, Arrow):
return obj.for_json() return obj.for_json()
elif isinstance(obj, date):
return obj.strftime("%Y-%m-%d")
elif isinstance(obj, datetime): elif isinstance(obj, datetime):
return obj.strftime(DATETIME_PRINT_FORMAT) return obj.strftime(DATETIME_PRINT_FORMAT)
elif isinstance(obj, date):
return obj.strftime("%Y-%m-%d")
iterable = iter(obj) iterable = iter(obj)
except TypeError: except TypeError:
pass pass
@ -108,7 +111,7 @@ class ApiServer(RPC):
'jwt_secret_key', 'super-secret') 'jwt_secret_key', 'super-secret')
self.jwt = JWTManager(self.app) self.jwt = JWTManager(self.app)
self.app.json_encoder = ArrowJSONEncoder self.app.json_encoder = FTJSONEncoder
self.app.teardown_appcontext(shutdown_session) self.app.teardown_appcontext(shutdown_session)
@ -160,16 +163,12 @@ class ApiServer(RPC):
""" """
pass pass
def rest_dump(self, return_value): def rest_error(self, error_msg, error_code=502):
""" Helper function to jsonify object for a webserver """ return jsonify({"error": error_msg}), error_code
return jsonify(return_value)
def rest_error(self, error_msg):
return jsonify({"error": error_msg}), 502
def register_rest_rpc_urls(self): def register_rest_rpc_urls(self):
""" """
Registers flask app URLs that are calls to functonality in rpc.rpc. Registers flask app URLs that are calls to functionality in rpc.rpc.
First two arguments passed are /URL and 'Label' First two arguments passed are /URL and 'Label'
Label can be used as a shortcut when refactoring Label can be used as a shortcut when refactoring
@ -193,6 +192,7 @@ class ApiServer(RPC):
self.app.add_url_rule(f'{BASE_URI}/balance', 'balance', self.app.add_url_rule(f'{BASE_URI}/balance', 'balance',
view_func=self._balance, methods=['GET']) view_func=self._balance, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/count', 'count', view_func=self._count, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/count', 'count', view_func=self._count, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/locks', 'locks', view_func=self._locks, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/daily', 'daily', view_func=self._daily, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/daily', 'daily', view_func=self._daily, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/edge', 'edge', view_func=self._edge, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/edge', 'edge', view_func=self._edge, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/logs', 'log', view_func=self._get_logs, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/logs', 'log', view_func=self._get_logs, methods=['GET'])
@ -212,6 +212,20 @@ class ApiServer(RPC):
view_func=self._trades, methods=['GET']) view_func=self._trades, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/trades/<int:tradeid>', 'trades_delete', self.app.add_url_rule(f'{BASE_URI}/trades/<int:tradeid>', 'trades_delete',
view_func=self._trades_delete, methods=['DELETE']) view_func=self._trades_delete, methods=['DELETE'])
self.app.add_url_rule(f'{BASE_URI}/pair_candles', 'pair_candles',
view_func=self._analysed_candles, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/pair_history', 'pair_history',
view_func=self._analysed_history, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/plot_config', 'plot_config',
view_func=self._plot_config, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/strategies', 'strategies',
view_func=self._list_strategies, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/strategy/<string:strategy>', 'strategy',
view_func=self._get_strategy, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/available_pairs', 'pairs',
view_func=self._list_available_pairs, methods=['GET'])
# Combined actions and infos # Combined actions and infos
self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist, self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist,
methods=['GET', 'POST']) methods=['GET', 'POST'])
@ -227,7 +241,7 @@ class ApiServer(RPC):
""" """
Return "404 not found", 404. Return "404 not found", 404.
""" """
return self.rest_dump({ return jsonify({
'status': 'error', 'status': 'error',
'reason': f"There's no API call for {request.base_url}.", 'reason': f"There's no API call for {request.base_url}.",
'code': 404 'code': 404
@ -247,7 +261,7 @@ class ApiServer(RPC):
'access_token': create_access_token(identity=keystuff), 'access_token': create_access_token(identity=keystuff),
'refresh_token': create_refresh_token(identity=keystuff), 'refresh_token': create_refresh_token(identity=keystuff),
} }
return self.rest_dump(ret) return jsonify(ret)
return jsonify({"error": "Unauthorized"}), 401 return jsonify({"error": "Unauthorized"}), 401
@ -262,7 +276,7 @@ class ApiServer(RPC):
new_token = create_access_token(identity=current_user, fresh=False) new_token = create_access_token(identity=current_user, fresh=False)
ret = {'access_token': new_token} ret = {'access_token': new_token}
return self.rest_dump(ret) return jsonify(ret)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -272,7 +286,7 @@ class ApiServer(RPC):
Starts TradeThread in bot if stopped. Starts TradeThread in bot if stopped.
""" """
msg = self._rpc_start() msg = self._rpc_start()
return self.rest_dump(msg) return jsonify(msg)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -282,7 +296,7 @@ class ApiServer(RPC):
Stops TradeThread in bot if running Stops TradeThread in bot if running
""" """
msg = self._rpc_stop() msg = self._rpc_stop()
return self.rest_dump(msg) return jsonify(msg)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -292,14 +306,14 @@ class ApiServer(RPC):
Sets max_open_trades to 0 and gracefully sells all open trades Sets max_open_trades to 0 and gracefully sells all open trades
""" """
msg = self._rpc_stopbuy() msg = self._rpc_stopbuy()
return self.rest_dump(msg) return jsonify(msg)
@rpc_catch_errors @rpc_catch_errors
def _ping(self): def _ping(self):
""" """
simple poing version simple ping version
""" """
return self.rest_dump({"status": "pong"}) return jsonify({"status": "pong"})
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -307,7 +321,7 @@ class ApiServer(RPC):
""" """
Prints the bot's version Prints the bot's version
""" """
return self.rest_dump({"version": __version__}) return jsonify({"version": __version__})
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -315,7 +329,7 @@ class ApiServer(RPC):
""" """
Prints the bot's version Prints the bot's version
""" """
return self.rest_dump(self._rpc_show_config()) return jsonify(self._rpc_show_config(self._config))
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -325,7 +339,7 @@ class ApiServer(RPC):
Triggers a config file reload Triggers a config file reload
""" """
msg = self._rpc_reload_config() msg = self._rpc_reload_config()
return self.rest_dump(msg) return jsonify(msg)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -335,7 +349,16 @@ class ApiServer(RPC):
Returns the number of trades running Returns the number of trades running
""" """
msg = self._rpc_count() msg = self._rpc_count()
return self.rest_dump(msg) return jsonify(msg)
@require_login
@rpc_catch_errors
def _locks(self):
"""
Handler for /locks.
Returns the currently active locks.
"""
return jsonify(self._rpc_locks())
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -353,7 +376,7 @@ class ApiServer(RPC):
self._config.get('fiat_display_currency', '') self._config.get('fiat_display_currency', '')
) )
return self.rest_dump(stats) return jsonify(stats)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -365,7 +388,7 @@ class ApiServer(RPC):
limit: Only get a certain number of records limit: Only get a certain number of records
""" """
limit = int(request.args.get('limit', 0)) or None limit = int(request.args.get('limit', 0)) or None
return self.rest_dump(self._rpc_get_logs(limit)) return jsonify(self._rpc_get_logs(limit))
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -376,7 +399,7 @@ class ApiServer(RPC):
""" """
stats = self._rpc_edge() stats = self._rpc_edge()
return self.rest_dump(stats) return jsonify(stats)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -392,7 +415,7 @@ class ApiServer(RPC):
self._config.get('fiat_display_currency') self._config.get('fiat_display_currency')
) )
return self.rest_dump(stats) return jsonify(stats)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -405,7 +428,7 @@ class ApiServer(RPC):
""" """
stats = self._rpc_performance() stats = self._rpc_performance()
return self.rest_dump(stats) return jsonify(stats)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -417,9 +440,9 @@ class ApiServer(RPC):
""" """
try: try:
results = self._rpc_trade_status() results = self._rpc_trade_status()
return self.rest_dump(results) return jsonify(results)
except RPCException: except RPCException:
return self.rest_dump([]) return jsonify([])
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -431,7 +454,7 @@ class ApiServer(RPC):
""" """
results = self._rpc_balance(self._config['stake_currency'], results = self._rpc_balance(self._config['stake_currency'],
self._config.get('fiat_display_currency', '')) self._config.get('fiat_display_currency', ''))
return self.rest_dump(results) return jsonify(results)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -443,7 +466,7 @@ class ApiServer(RPC):
""" """
limit = int(request.args.get('limit', 0)) limit = int(request.args.get('limit', 0))
results = self._rpc_trade_history(limit) results = self._rpc_trade_history(limit)
return self.rest_dump(results) return jsonify(results)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -456,7 +479,7 @@ class ApiServer(RPC):
tradeid: Numeric trade-id assigned to the trade. tradeid: Numeric trade-id assigned to the trade.
""" """
result = self._rpc_delete(tradeid) result = self._rpc_delete(tradeid)
return self.rest_dump(result) return jsonify(result)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -465,7 +488,7 @@ class ApiServer(RPC):
Handler for /whitelist. Handler for /whitelist.
""" """
results = self._rpc_whitelist() results = self._rpc_whitelist()
return self.rest_dump(results) return jsonify(results)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -475,7 +498,7 @@ class ApiServer(RPC):
""" """
add = request.json.get("blacklist", None) if request.method == 'POST' else None add = request.json.get("blacklist", None) if request.method == 'POST' else None
results = self._rpc_blacklist(add) results = self._rpc_blacklist(add)
return self.rest_dump(results) return jsonify(results)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -487,9 +510,9 @@ class ApiServer(RPC):
price = request.json.get("price", None) price = request.json.get("price", None)
trade = self._rpc_forcebuy(asset, price) trade = self._rpc_forcebuy(asset, price)
if trade: if trade:
return self.rest_dump(trade.to_json()) return jsonify(trade.to_json())
else: else:
return self.rest_dump({"status": f"Error buying pair {asset}."}) return jsonify({"status": f"Error buying pair {asset}."})
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
@ -499,4 +522,132 @@ class ApiServer(RPC):
""" """
tradeid = request.json.get("tradeid") tradeid = request.json.get("tradeid")
results = self._rpc_forcesell(tradeid) results = self._rpc_forcesell(tradeid)
return self.rest_dump(results) return jsonify(results)
@require_login
@rpc_catch_errors
def _analysed_candles(self):
"""
Handler for /pair_candles.
Returns the dataframe the bot is using during live/dry operations.
Takes the following get arguments:
get:
parameters:
- pair: Pair
- timeframe: Timeframe to get data for (should be aligned to strategy.timeframe)
- limit: Limit return length to the latest X candles
"""
pair = request.args.get("pair")
timeframe = request.args.get("timeframe")
limit = request.args.get("limit", type=int)
if not pair or not timeframe:
return self.rest_error("Mandatory parameter missing.", 400)
results = self._rpc_analysed_dataframe(pair, timeframe, limit)
return jsonify(results)
@require_login
@rpc_catch_errors
def _analysed_history(self):
"""
Handler for /pair_history.
Returns the dataframe of a given timerange
Takes the following get arguments:
get:
parameters:
- pair: Pair
- timeframe: Timeframe to get data for (should be aligned to strategy.timeframe)
- strategy: Strategy to use - Must exist in configured strategy-path!
- timerange: timerange in the format YYYYMMDD-YYYYMMDD (YYYYMMDD- or (-YYYYMMDD))
are als possible. If omitted uses all available data.
"""
pair = request.args.get("pair")
timeframe = request.args.get("timeframe")
timerange = request.args.get("timerange")
strategy = request.args.get("strategy")
if not pair or not timeframe or not timerange or not strategy:
return self.rest_error("Mandatory parameter missing.", 400)
config = deepcopy(self._config)
config.update({
'strategy': strategy,
})
results = RPC._rpc_analysed_history_full(config, pair, timeframe, timerange)
return jsonify(results)
@require_login
@rpc_catch_errors
def _plot_config(self):
"""
Handler for /plot_config.
"""
return jsonify(self._rpc_plot_config())
@require_login
@rpc_catch_errors
def _list_strategies(self):
directory = Path(self._config.get(
'strategy_path', self._config['user_data_dir'] / USERPATH_STRATEGIES))
from freqtrade.resolvers.strategy_resolver import StrategyResolver
strategy_objs = StrategyResolver.search_all_objects(directory, False)
strategy_objs = sorted(strategy_objs, key=lambda x: x['name'])
return jsonify({'strategies': [x['name'] for x in strategy_objs]})
@require_login
@rpc_catch_errors
def _get_strategy(self, strategy: str):
"""
Get a single strategy
get:
parameters:
- strategy: Only get this strategy
"""
config = deepcopy(self._config)
from freqtrade.resolvers.strategy_resolver import StrategyResolver
try:
strategy_obj = StrategyResolver._load_strategy(strategy, config,
extra_dir=config.get('strategy_path'))
except OperationalException:
return self.rest_error("Strategy not found.", 404)
return jsonify({
'strategy': strategy_obj.get_strategy_name(),
'code': strategy_obj.__source__,
})
@require_login
@rpc_catch_errors
def _list_available_pairs(self):
"""
Handler for /available_pairs.
Returns an object, with pairs, available pair length and pair_interval combinations
Takes the following get arguments:
get:
parameters:
- stake_currency: Filter on this stake currency
- timeframe: Timeframe to get data for Filter elements to this timeframe
"""
timeframe = request.args.get("timeframe")
stake_currency = request.args.get("stake_currency")
from freqtrade.data.history import get_datahandler
dh = get_datahandler(self._config['datadir'], self._config.get('dataformat_ohlcv', None))
pair_interval = dh.ohlcv_get_available_data(self._config['datadir'])
if timeframe:
pair_interval = [pair for pair in pair_interval if pair[1] == timeframe]
if stake_currency:
pair_interval = [pair for pair in pair_interval if pair[0].endswith(stake_currency)]
pair_interval = sorted(pair_interval, key=lambda x: x[0])
pairs = list({x[0] for x in pair_interval})
result = {
'length': len(pairs),
'pairs': pairs,
'pair_interval': pair_interval,
}
return jsonify(result)

View File

@ -9,18 +9,22 @@ from math import isnan
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
import arrow import arrow
from numpy import NAN, mean from numpy import NAN, int64, mean
from pandas import DataFrame
from freqtrade.constants import CANCEL_REASON from freqtrade.configuration.timerange import TimeRange
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT
from freqtrade.data.history import load_data
from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exceptions import ExchangeError, PricingError
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
from freqtrade.loggers import bufferHandler from freqtrade.loggers import bufferHandler
from freqtrade.misc import shorten_date from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade from freqtrade.persistence import PairLocks, Trade
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.state import State from freqtrade.state import State
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -89,13 +93,12 @@ class RPC:
def send_msg(self, msg: Dict[str, str]) -> None: def send_msg(self, msg: Dict[str, str]) -> None:
""" Sends a message to all registered rpc modules """ """ Sends a message to all registered rpc modules """
def _rpc_show_config(self) -> Dict[str, Any]: def _rpc_show_config(self, config) -> Dict[str, Any]:
""" """
Return a dict of config options. Return a dict of config options.
Explicitly does NOT return the full config to avoid leakage of sensitive Explicitly does NOT return the full config to avoid leakage of sensitive
information via rpc. information via rpc.
""" """
config = self._freqtrade.config
val = { val = {
'dry_run': config['dry_run'], 'dry_run': config['dry_run'],
'stake_currency': config['stake_currency'], 'stake_currency': config['stake_currency'],
@ -116,7 +119,7 @@ class RPC:
'forcebuy_enabled': config.get('forcebuy_enable', False), 'forcebuy_enabled': config.get('forcebuy_enable', False),
'ask_strategy': config.get('ask_strategy', {}), 'ask_strategy': config.get('ask_strategy', {}),
'bid_strategy': config.get('bid_strategy', {}), 'bid_strategy': config.get('bid_strategy', {}),
'state': str(self._freqtrade.state) 'state': str(self._freqtrade.state) if self._freqtrade else '',
} }
return val return val
@ -596,6 +599,17 @@ class RPC:
'total_stake': sum((trade.open_rate * trade.amount) for trade in trades) 'total_stake': sum((trade.open_rate * trade.amount) for trade in trades)
} }
def _rpc_locks(self) -> Dict[str, Any]:
""" Returns the current locks"""
if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running')
locks = PairLocks.get_pair_locks(None)
return {
'lock_count': len(locks),
'locks': [lock.to_json() for lock in locks]
}
def _rpc_whitelist(self) -> Dict: def _rpc_whitelist(self) -> Dict:
""" Returns the currently active whitelist""" """ Returns the currently active whitelist"""
res = {'method': self._freqtrade.pairlists.name_list, res = {'method': self._freqtrade.pairlists.name_list,
@ -635,7 +649,7 @@ class RPC:
buffer = bufferHandler.buffer[-limit:] buffer = bufferHandler.buffer[-limit:]
else: else:
buffer = bufferHandler.buffer buffer = bufferHandler.buffer
records = [[datetime.fromtimestamp(r.created).strftime("%Y-%m-%d %H:%M:%S"), records = [[datetime.fromtimestamp(r.created).strftime(DATETIME_PRINT_FORMAT),
r.created * 1000, r.name, r.levelname, r.created * 1000, r.name, r.levelname,
r.message + ('\n' + r.exc_text if r.exc_text else '')] r.message + ('\n' + r.exc_text if r.exc_text else '')]
for r in buffer] for r in buffer]
@ -652,3 +666,82 @@ class RPC:
if not self._freqtrade.edge: if not self._freqtrade.edge:
raise RPCException('Edge is not enabled.') raise RPCException('Edge is not enabled.')
return self._freqtrade.edge.accepted_pairs() return self._freqtrade.edge.accepted_pairs()
@staticmethod
def _convert_dataframe_to_dict(strategy: str, pair: str, timeframe: str, dataframe: DataFrame,
last_analyzed: datetime) -> Dict[str, Any]:
has_content = len(dataframe) != 0
buy_signals = 0
sell_signals = 0
if has_content:
dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].astype(int64) // 1000 // 1000
# Move open to seperate column when signal for easy plotting
if 'buy' in dataframe.columns:
buy_mask = (dataframe['buy'] == 1)
buy_signals = int(buy_mask.sum())
dataframe.loc[buy_mask, '_buy_signal_open'] = dataframe.loc[buy_mask, 'open']
if 'sell' in dataframe.columns:
sell_mask = (dataframe['sell'] == 1)
sell_signals = int(sell_mask.sum())
dataframe.loc[sell_mask, '_sell_signal_open'] = dataframe.loc[sell_mask, 'open']
dataframe = dataframe.replace({NAN: None})
res = {
'pair': pair,
'timeframe': timeframe,
'timeframe_ms': timeframe_to_msecs(timeframe),
'strategy': strategy,
'columns': list(dataframe.columns),
'data': dataframe.values.tolist(),
'length': len(dataframe),
'buy_signals': buy_signals,
'sell_signals': sell_signals,
'last_analyzed': last_analyzed,
'last_analyzed_ts': int(last_analyzed.timestamp()),
'data_start': '',
'data_start_ts': 0,
'data_stop': '',
'data_stop_ts': 0,
}
if has_content:
res.update({
'data_start': str(dataframe.iloc[0]['date']),
'data_start_ts': int(dataframe.iloc[0]['__date_ts']),
'data_stop': str(dataframe.iloc[-1]['date']),
'data_stop_ts': int(dataframe.iloc[-1]['__date_ts']),
})
return res
def _rpc_analysed_dataframe(self, pair: str, timeframe: str, limit: int) -> Dict[str, Any]:
_data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(
pair, timeframe)
_data = _data.copy()
if limit:
_data = _data.iloc[-limit:]
return self._convert_dataframe_to_dict(self._freqtrade.config['strategy'],
pair, timeframe, _data, last_analyzed)
@staticmethod
def _rpc_analysed_history_full(config, pair: str, timeframe: str,
timerange: str) -> Dict[str, Any]:
timerange_parsed = TimeRange.parse_timerange(timerange)
_data = load_data(
datadir=config.get("datadir"),
pairs=[pair],
timeframe=timeframe,
timerange=timerange_parsed,
data_format=config.get('dataformat_ohlcv', 'json'),
)
from freqtrade.resolvers.strategy_resolver import StrategyResolver
strategy = StrategyResolver.load_strategy(config)
df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair})
return RPC._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe,
df_analyzed, arrow.Arrow.utcnow().datetime)
def _rpc_plot_config(self) -> Dict[str, Any]:
return self._freqtrade.strategy.plot_config

View File

@ -6,6 +6,7 @@ from typing import Any, Dict, List
from freqtrade.rpc import RPC, RPCMessageType from freqtrade.rpc import RPC, RPCMessageType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -5,9 +5,9 @@ This module manage Telegram communication
""" """
import json import json
import logging import logging
import arrow
from typing import Any, Callable, Dict from typing import Any, Callable, Dict
import arrow
from tabulate import tabulate from tabulate import tabulate
from telegram import ParseMode, ReplyKeyboardMarkup, Update from telegram import ParseMode, ReplyKeyboardMarkup, Update
from telegram.error import NetworkError, TelegramError from telegram.error import NetworkError, TelegramError
@ -18,6 +18,7 @@ from freqtrade.__init__ import __version__
from freqtrade.rpc import RPC, RPCException, RPCMessageType from freqtrade.rpc import RPC, RPCException, RPCMessageType
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.debug('Included module rpc.telegram ...') logger.debug('Included module rpc.telegram ...')
@ -99,6 +100,7 @@ class Telegram(RPC):
CommandHandler('performance', self._performance), CommandHandler('performance', self._performance),
CommandHandler('daily', self._daily), CommandHandler('daily', self._daily),
CommandHandler('count', self._count), CommandHandler('count', self._count),
CommandHandler('locks', self._locks),
CommandHandler(['reload_config', 'reload_conf'], self._reload_config), CommandHandler(['reload_config', 'reload_conf'], self._reload_config),
CommandHandler(['show_config', 'show_conf'], self._show_config), CommandHandler(['show_config', 'show_conf'], self._show_config),
CommandHandler('stopbuy', self._stopbuy), CommandHandler('stopbuy', self._stopbuy),
@ -607,6 +609,26 @@ class Telegram(RPC):
except RPCException as e: except RPCException as e:
self._send_msg(str(e)) self._send_msg(str(e))
@authorized_only
def _locks(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /locks.
Returns the currently active locks
"""
try:
locks = self._rpc_locks()
message = tabulate([[
lock['pair'],
lock['lock_end_time'],
lock['reason']] for lock in locks['locks']],
headers=['Pair', 'Until', 'Reason'],
tablefmt='simple')
message = "<pre>{}</pre>".format(message)
logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e))
@authorized_only @authorized_only
def _whitelist(self, update: Update, context: CallbackContext) -> None: def _whitelist(self, update: Update, context: CallbackContext) -> None:
""" """
@ -718,8 +740,8 @@ class Telegram(RPC):
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n" "*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
"*/performance:* `Show performance of each finished trade grouped by pair`\n" "*/performance:* `Show performance of each finished trade grouped by pair`\n"
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n" "*/daily <n>:* `Shows profit or loss per day, over the last n days`\n"
"*/count:* `Show number of trades running compared to allowed number of trades`" "*/count:* `Show number of active trades compared to allowed number of trades`\n"
"\n" "*/locks:* `Show currently locked pairs`\n"
"*/balance:* `Show account balance per currency`\n" "*/balance:* `Show account balance per currency`\n"
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
"*/reload_config:* `Reload configuration file` \n" "*/reload_config:* `Reload configuration file` \n"
@ -754,7 +776,7 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
val = self._rpc_show_config() val = self._rpc_show_config(self._freqtrade.config)
if val['trailing_stop']: if val['trailing_stop']:
sl_info = ( sl_info = (
f"*Initial Stoploss:* `{val['stoploss']}`\n" f"*Initial Stoploss:* `{val['stoploss']}`\n"

View File

@ -4,7 +4,7 @@ This module manages webhook communication
import logging import logging
from typing import Any, Dict from typing import Any, Dict
from requests import post, RequestException from requests import RequestException, post
from freqtrade.rpc import RPC, RPCMessageType from freqtrade.rpc import RPC, RPCMessageType

View File

@ -1,5 +1,5 @@
# flake8: noqa: F401 # flake8: noqa: F401
from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_prev_date, from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date,
timeframe_to_seconds, timeframe_to_next_date, timeframe_to_msecs) timeframe_to_prev_date, timeframe_to_seconds)
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_helper import merge_informative_pair from freqtrade.strategy.strategy_helper import merge_informative_pair

Some files were not shown because too many files have changed in this diff Show More