diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4ef3ba7e..f259129d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,14 +19,14 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-18.04, macos-latest ] + os: [ ubuntu-18.04, ubuntu-20.04, macos-latest ] python-version: [3.7, 3.8] steps: - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} @@ -70,7 +70,7 @@ jobs: pytest --random-order --cov=freqtrade --cov-config=.coveragerc - name: Coveralls - if: (startsWith(matrix.os, 'ubuntu') && matrix.python-version == '3.8') + if: (startsWith(matrix.os, 'ubuntu-20') && matrix.python-version == '3.8') env: # Coveralls token. Not used as secret due to github not providing secrets to forked repositories COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu @@ -88,12 +88,16 @@ jobs: run: | cp config.json.example config.json 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 run: | flake8 + - name: Sort imports (isort) + run: | + isort --check . + - name: Mypy run: | mypy freqtrade scripts @@ -121,7 +125,7 @@ jobs: - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} @@ -150,7 +154,7 @@ jobs: run: | cp config.json.example config.json 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 run: | @@ -172,7 +176,7 @@ jobs: url: ${{ secrets.SLACK_WEBHOOK }} docs_check: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 @@ -180,6 +184,17 @@ jobs: run: | ./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 uses: homoluctus/slatify@v1.8.0 if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) @@ -190,7 +205,7 @@ jobs: url: ${{ secrets.SLACK_WEBHOOK }} cleanup-prior-runs: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Cleanup previous runs on this branch 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-complete: needs: [ build, build_windows, docs_check ] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Slack Notification uses: homoluctus/slatify@v1.8.0 @@ -214,13 +229,13 @@ jobs: deploy: 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' steps: - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: 3.8 diff --git a/.travis.yml b/.travis.yml index 0cb76b78b..9b8448db5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,7 +33,7 @@ jobs: - script: - cp config.json.example config.json - 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 - script: flake8 name: flake8 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 97f62154d..399588f88 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,6 +65,14 @@ Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using mypy freqtrade ``` +### 4. Ensure all imports are correct + +#### Run isort + +``` bash +isort . +``` + ## (Core)-Committer Guide ### Process: Pull Requests diff --git a/README.md b/README.md index feea47299..c9f4d0a52 100644 --- a/README.md +++ b/README.md @@ -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. ```bash -git clone git@github.com:freqtrade/freqtrade.git +git clone -b develop https://github.com/freqtrade/freqtrade.git cd freqtrade -git checkout develop ./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/) -- `/start`: Starts the trader -- `/stop`: Stops the trader -- `/status [table]`: Lists all open trades -- `/count`: Displays number of open trades +- `/start`: Starts the trader. +- `/stop`: Stops the trader. +- `/stopbuy`: Stop entering new trades. +- `/status [table]`: Lists all open trades. - `/profit`: Lists cumulative profit from all finished trades - `/forcesell |all`: Instantly sells the given trade (Ignoring `minimum_roi`). - `/performance`: Show performance of each finished trade grouped by pair -- `/balance`: Show account balance per currency -- `/daily `: Shows profit or loss per day, over the last n days -- `/help`: Show help message -- `/version`: Show version +- `/balance`: Show account balance per currency. +- `/daily `: Shows profit or loss per day, over the last n days. +- `/help`: Show help message. +- `/version`: Show version. ## Development branches @@ -133,13 +132,16 @@ The project is currently setup in two main branches: ## 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 slack channel. +For any questions not covered by the documentation or for further 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). +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) 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) 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. **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 -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 diff --git a/build_helpers/TA_Lib-0.4.18-cp37-cp37m-win_amd64.whl b/build_helpers/TA_Lib-0.4.18-cp37-cp37m-win_amd64.whl deleted file mode 100644 index bd61e812b..000000000 Binary files a/build_helpers/TA_Lib-0.4.18-cp37-cp37m-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.18-cp38-cp38-win_amd64.whl b/build_helpers/TA_Lib-0.4.18-cp38-cp38-win_amd64.whl deleted file mode 100644 index f81addb44..000000000 Binary files a/build_helpers/TA_Lib-0.4.18-cp38-cp38-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl b/build_helpers/TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl new file mode 100644 index 000000000..5adbda8a7 Binary files /dev/null and b/build_helpers/TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl differ diff --git a/build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl b/build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl new file mode 100644 index 000000000..b652c7ee0 Binary files /dev/null and b/build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl differ diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 index 0a55b6ddd..5747db335 100644 --- a/build_helpers/install_windows.ps1 +++ b/build_helpers/install_windows.ps1 @@ -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}')" 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') { - 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 diff --git a/config.json.example b/config.json.example index ab517b77c..af45dac74 100644 --- a/config.json.example +++ b/config.json.example @@ -5,15 +5,15 @@ "tradable_balance_ratio": 0.99, "fiat_display_currency": "USD", "timeframe": "5m", - "dry_run": false, + "dry_run": true, "cancel_open_orders_on_exit": false, "unfilledtimeout": { "buy": 10, "sell": 30 }, "bid_strategy": { - "ask_last_balance": 0.0, "use_order_book": false, + "ask_last_balance": 0.0, "order_book_top": 1, "check_depth_of_market": { "enabled": false, diff --git a/config_full.json.example b/config_full.json.example index 659580fb1..45c5c695c 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -7,7 +7,7 @@ "amount_reserve_percent": 0.05, "amend_last_stake_amount": false, "last_stake_amount_min_ratio": 0.5, - "dry_run": false, + "dry_run": true, "cancel_open_orders_on_exit": false, "timeframe": "5m", "trailing_stop": false, diff --git a/config_kraken.json.example b/config_kraken.json.example index fd0b2b95d..5f3b57854 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -27,12 +27,11 @@ "use_sell_signal": true, "sell_profit_only": false, "ignore_roi_if_buy_signal": false - }, "exchange": { "name": "kraken", - "key": "", - "secret": "", + "key": "your_exchange_key", + "secret": "your_exchange_key", "ccxt_config": {"enableRateLimit": true}, "ccxt_async_config": { "enableRateLimit": true, diff --git a/docker/Dockerfile.plot b/docker/Dockerfile.plot index 1843efdcb..40bc72bc5 100644 --- a/docker/Dockerfile.plot +++ b/docker/Dockerfile.plot @@ -5,6 +5,3 @@ FROM freqtradeorg/freqtrade:${sourceimage} COPY requirements-plot.txt /freqtrade/ RUN pip install -r requirements-plot.txt --no-cache-dir - -# Empty the ENTRYPOINT to allow all commands -ENTRYPOINT [] diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 5fc674b03..59ebc16b5 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -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: ``` -$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt ... +$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ... or -$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 ... +$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ... ``` ## Creating and using a custom loss function diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 4a4496bbc..4d07435c7 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -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} ...]] [--dmmp] [--print-all] [--no-color] [--print-json] [-j JOBS] [--random-state INT] [--min-trades INT] - [--continue] [--hyperopt-loss NAME] + [--hyperopt-loss NAME] optional arguments: -h, --help show this help message and exit @@ -349,18 +349,14 @@ optional arguments: reproducible hyperopt results. --min-trades INT Set minimal desired number of trades for evaluations 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 class (IHyperOptLoss). Different functions can generate completely different results, since the target for optimization is different. Built-in - Hyperopt-loss-functions are: DefaultHyperOptLoss, + Hyperopt-loss-functions are: ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, SortinoHyperOptLoss, - SortinoHyperOptLossDaily.(default: - `DefaultHyperOptLoss`). + SortinoHyperOptLossDaily. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/docs/configuration.md b/docs/configuration.md index d6e26f80e..47362e525 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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).
**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).
*Defaults to `0.0` (no offset).*
**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).
*Defaults to `false`.*
**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).
**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).
**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).
**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).
**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).
*Defaults to `bid`.*
**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.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled).
**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. - -## 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} - ], -``` +--8<-- "includes/pairlists.md" ## Switch to Dry-run mode diff --git a/docs/developer.md b/docs/developer.md index 788e961cd..8ef816d5d 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -96,7 +96,7 @@ Below is an outline of exception inheritance hierarchy: ## Modules -### Dynamic Pairlist +### Pairlists 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. diff --git a/docs/edge.md b/docs/edge.md index 500c3c833..7442f1927 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -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}} $$ ???+ 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.
- Your potential profit is calculated as:
+ 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: + $\begin{aligned} - \text{potential_profit} &= (\text{potential_price} - \text{cost_per_unit}) * \frac{\text{investment}}{\text{cost_per_unit}} \\ - &= (15 - 10) * \frac{100}{15}\\ - &= 33.33 - \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:
+ \text{potential_profit} &= (\text{potential_price} - \text{entry_price}) * \frac{\text{investment}}{\text{entry_price}} \\ + &= (15 - 10) * (100 / 10) \\ + &= 50 + \end{aligned}$ + + 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} R &= \frac{\text{potential_profit}}{\text{potential_loss}}\\ - &= \frac{33.33}{100}\\ - &= 0.333... + &= \frac{50}{15}\\ + &= 3.33 \end{aligned}$
- 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: diff --git a/docs/faq.md b/docs/faq.md index beed89801..a775060de 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -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. ```bash -freqtrade hyperopt -e 1000 -``` - -or if you want intermediate result to see - -```bash -for i in {1..100}; do freqtrade hyperopt -e 1000; done +freqtrade hyperopt --hyperop SampleHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000 ``` ### Why does it take a long time to run hyperopt? diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 3f7a27ef0..fc7a0dd93 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -37,12 +37,20 @@ pip install -r requirements-hyperopt.txt 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). -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`. -This will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. +!!! Tip "About this page" + 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: @@ -54,17 +62,15 @@ Depending on the space you want to optimize, only some of the below are required !!! Note `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_buy_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 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: @@ -80,17 +86,20 @@ Rarely you may also need to override: # Have a working strategy at hand. 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`: -Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts/awesome_hyperopt.py` +``` bash +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: @@ -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. 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 -multiple guards. The constructed strategy will be something like -"*buy exactly when close price touches lower Bollinger band, BUT only if +!!! Hint "Guards and Triggers" + Technically, there is no difference between Guards and Triggers. + 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*". -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. +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. #### 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 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 @@ -154,7 +165,7 @@ We will start by defining a search space: Above definition says: I have five parameters I want you to randomly combine to find the best combination. Two of them are integer values (`adx-value` -and `rsi-value`) and I want you test in the range of values 20 to 40. +and `rsi-value`) and I want you test in the range of values 20 to 40. Then we have three category variables. First two are either `True` or `False`. We use these to either enable or disable the ADX and RSI guards. The last one we call `trigger` and use it to decide which buy trigger we want to use. @@ -192,27 +203,25 @@ So let's write the buy strategy using these values: return populate_buy_trend ``` -Hyperopting will now call this `populate_buy_trend` as many times you ask it (`epochs`) -with different value combinations. It will then use the given historical data and make -buys based on the buy signals generated with the above function and based on the results -it will end with telling you which parameter combination produced the best profits. +Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. +It will use the given historical data and make buys based on the buy signals generated with the above function. +Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)). -The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. -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 custom hyperopt file. +!!! Note + The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. + 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 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 different loss function can be specified by using the `--hyperopt-loss ` argument. +A loss function must be specified via the `--hyperopt-loss ` argument (or optionally via the configuration under the `"hyperopt_loss"` key). This class should be in its own file within the `user_data/hyperopts/` directory. 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) * `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) @@ -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. ```bash -freqtrade hyperopt --config config.json --hyperopt -e 500 --spaces all +freqtrade hyperopt --config config.json --hyperopt --hyperopt-loss --strategy -e 500 --spaces all ``` Use `` as the name of the custom hyperopt used. -The `-e` option will set how many evaluations hyperopt will do. We recommend -running at least several thousand evaluations. +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. +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. !!! Note - By default, hyperopt will erase previous results and start from scratch. Continuation can be archived by using `--continue`. - -!!! Warning - When switching parameters or changing configuration options, make sure to not use the argument `--continue` so temporary results can be removed. + Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. + Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. + You can find a list of filenames with `ls -l user_data/hyperopt_results/`. ### 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 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: ```bash -freqtrade hyperopt --timerange 20180401-20180501 +freqtrade hyperopt --hyperopt --strategy --timerange 20180401-20180501 ``` ### 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. ```bash -freqtrade hyperopt --strategy SampleStrategy --hyperopt SampleHyperopt +freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy ``` ### Running Hyperopt with Smaller Search Space Use the `--spaces` option to limit the search space used by hyperopt. -Letting Hyperopt optimize everything is a huuuuge search space. 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 -new buy strategy you have. +Letting Hyperopt optimize everything is a huuuuge search space. +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 new buy strategy you have. 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 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 @@ -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. !!! 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 @@ -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. -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 @@ -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: -``` +``` python # Optimal stoploss designed for the strategy # This attribute will be overridden if the config file contains "stoploss" 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: -``` +``` python # Trailing stop # These attributes will be overridden if the config file contains corresponding values. 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 -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 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`). diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md new file mode 100644 index 000000000..ae4ec818d --- /dev/null +++ b/docs/includes/pairlists.md @@ -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} + ], +``` diff --git a/docs/index.md b/docs/index.md index e7fc54628..5608587db 100644 --- a/docs/index.md +++ b/docs/index.md @@ -59,11 +59,17 @@ Alternatively ## 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. 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? -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). diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index d4c93928e..f30710a1f 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,3 @@ -mkdocs-material==5.5.13 +mkdocs-material==6.1.0 mdx_truly_sane_lists==1.2 +pymdown-extensions==8.0.1 diff --git a/docs/rest-api.md b/docs/rest-api.md index 075bd7e64..7726ab875 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -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 [optional parameters] ``` -## Available commands +## Available endpoints | Command | Description | |----------|-------------| | `ping` | Simple command testing the API Readiness - requires no authentication. -| `start` | Starts the trader -| `stop` | Stops the trader +| `start` | Starts the trader. +| `stop` | Stops the trader. | `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. | `delete_trade ` | 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 -| `logs` | Shows last log messages -| `status` | Lists all open trades -| `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 +| `show_config` | Shows part of the current configuration with relevant settings to operation. +| `logs` | Shows last log messages. +| `status` | Lists all open trades. +| `count` | Displays number of trades used and available. +| `locks` | Displays currently locked pairs. +| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance. | `forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). | `forcebuy [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 -| `balance` | Show account balance per currency -| `daily ` | Shows profit or loss per day, over the last n days (n defaults to 7) -| `whitelist` | Show the current whitelist +| `performance` | Show performance of each finished trade grouped by pair. +| `balance` | Show account balance per currency. +| `daily ` | Shows profit or loss per day, over the last n days (n defaults to 7). +| `whitelist` | Show the current whitelist. | `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `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 ` | 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. @@ -140,6 +150,12 @@ python3 scripts/rest_client.py help ``` output 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 Get the account balance. @@ -179,9 +195,27 @@ logs :param limit: Limits log messages to the last logs. No limit to get all the trades. +pair_candles + Return live dataframe for . + + :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 Return the performance of the different coins. +plot_config + Return plot configuration if the strategy defines one. + profit Return the profit summary. @@ -204,6 +238,14 @@ stop stopbuy 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 Return trades history. @@ -215,7 +257,6 @@ version whitelist Show the current whitelist. - ``` ## Advanced API usage using JWT tokens diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 168d416ab..569af33ff 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -43,52 +43,6 @@ sqlite3 .schema ``` -### 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 ```sql @@ -98,11 +52,11 @@ SELECT * FROM trades; ## Fix trade still open after a manual sell on the exchange !!! Warning - Manually selling a pair on the exchange will not be detected by the bot and it will try to sell anyway. Whenever possible, forcesell should be used to accomplish the same thing. - It is strongly advised to backup your database file before making any manual changes. + Manually selling a pair on the exchange will not be detected by the bot and it will try to sell anyway. Whenever possible, forcesell should be used to accomplish the same thing. + It is strongly advised to backup your database file before making any manual changes. !!! Note - This should not be necessary after /forcesell, as forcesell orders are closed automatically by the bot on the next iteration. + This should not be necessary after /forcesell, as forcesell orders are closed automatically by the bot on the next iteration. ```sql UPDATE trades @@ -128,23 +82,12 @@ SET is_open=0, 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, , , , '') -``` - -### 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 -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 ` 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 DELETE FROM trades WHERE id = ; diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 14d5fcd84..6c7d78864 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -312,12 +312,17 @@ The name of the variable can be chosen at will, but should be prefixed with `cus class Awesomestrategy(IStrategy): # Create custom dictionary cust_info = {} + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # 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"]: - self.cust_info[metadata["pair"]["crosstime"] += 1 + self.cust_info[metadata["pair"]]["crosstime"] += 1 else: - self.cust_info[metadata["pair"]["crosstime"] = 1 + self.cust_info[metadata["pair"]]["crosstime"] = 1 ``` !!! Warning @@ -688,18 +693,18 @@ Locked pairs will show the message `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). -Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until)`. -`until` must be a datetime object in the future, after which trading will be reenabled for that pair. +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 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)`. To verify if a pair is currently locked, use `self.is_pair_locked(pair)`. !!! 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 - Locking pairs is not functioning during backtesting. + Locking pairs is not available during backtesting. #### Pair locking example diff --git a/docs/utils.md b/docs/utils.md index 8c7e381ff..409bcc134 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -423,7 +423,7 @@ freqtrade test-pairlist --config config.json --quote USDT BTC ## 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] @@ -432,10 +432,11 @@ usage: freqtrade hyperopt-list [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--max-trades INT] [--min-avg-time FLOAT] [--max-avg-time FLOAT] [--min-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] [--no-color] [--print-json] [--no-details] - [--export-csv FILE] + [--hyperopt-filename PATH] [--export-csv FILE] optional arguments: -h, --help show this help message and exit @@ -443,24 +444,27 @@ optional arguments: --profitable Select only profitable epochs. --min-trades INT Select epochs with more than INT trades. --max-trades INT Select epochs with less than INT trades. - --min-avg-time FLOAT Select epochs on above average time. - --max-avg-time FLOAT Select epochs on under average time. + --min-avg-time FLOAT Select epochs above average time. + --max-avg-time FLOAT Select epochs below average time. --min-avg-profit FLOAT - Select epochs on above average profit. + Select epochs above average profit. --max-avg-profit FLOAT - Select epochs on below average profit. + Select epochs below average profit. --min-total-profit FLOAT - Select epochs on above total profit. + Select epochs above total profit. --max-total-profit FLOAT - Select epochs on below total profit. + Select epochs below total profit. --min-objective FLOAT - Select epochs on above objective (- is added by default). + Select epochs above objective. --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 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. + --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. Example: --export-csv hyperopt.csv @@ -480,7 +484,11 @@ Common arguments: --userdir PATH, --user-data-dir PATH 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 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] [-d PATH] [--userdir PATH] [--best] [--profitable] [-n INT] [--print-json] - [--no-header] + [--hyperopt-filename PATH] [--no-header] optional arguments: -h, --help show this help message and exit --best Select only best epochs. --profitable Select only profitable epochs. -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. + +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 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): diff --git a/docs/windows_installation.md b/docs/windows_installation.md index f7900d85a..0ef0f131f 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -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). -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_Lib‑0.4.18‑cp38‑cp38‑win_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_Lib‑0.4.19‑cp38‑cp38‑win_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. Other versions must be downloaded from the above link. @@ -32,7 +32,7 @@ python -m venv .env .env\Scripts\activate.ps1 # optionally install ta-lib from wheel # Eventually adjust the below filename to match the downloaded wheel -pip install build_helpes/TA_Lib‑0.4.18‑cp38‑cp38‑win_amd64.whl +pip install build_helpes/TA_Lib‑0.4.19‑cp38‑cp38‑win_amd64.whl pip install -r requirements.txt pip install -e . freqtrade diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index f1b88805b..125dca1ef 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2020.9.1' +__version__ = '2020.10' if __version__ == 'develop': diff --git a/freqtrade/__main__.py b/freqtrade/__main__.py index 97ed9ae67..881a2f562 100644 --- a/freqtrade/__main__.py +++ b/freqtrade/__main__.py @@ -8,5 +8,6 @@ To launch Freqtrade as a module from freqtrade import main + if __name__ == '__main__': main.main() diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 4ce3eb421..21c5d6812 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -8,23 +8,15 @@ Note: Be careful with file-scoped imports in these subfiles. """ from freqtrade.commands.arguments import Arguments from freqtrade.commands.build_config_commands import start_new_config -from freqtrade.commands.data_commands import (start_convert_data, - start_download_data, +from freqtrade.commands.data_commands import (start_convert_data, start_download_data, start_list_data) -from freqtrade.commands.deploy_commands import (start_create_userdir, - start_new_hyperopt, +from freqtrade.commands.deploy_commands import (start_create_userdir, start_new_hyperopt, start_new_strategy) -from freqtrade.commands.hyperopt_commands import (start_hyperopt_list, - start_hyperopt_show) -from freqtrade.commands.list_commands import (start_list_exchanges, - start_list_hyperopts, - start_list_markets, - start_list_strategies, - start_list_timeframes, - start_show_trades) -from freqtrade.commands.optimize_commands import (start_backtesting, - start_edge, start_hyperopt) +from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show +from freqtrade.commands.list_commands import (start_list_exchanges, start_list_hyperopts, + start_list_markets, 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.plot_commands import (start_plot_dataframe, - start_plot_profit) +from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit from freqtrade.commands.trade_commands import start_trading diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index b61a4933e..aa58ff585 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -9,6 +9,7 @@ from typing import Any, Dict, List, Optional from freqtrade.commands.cli_options import AVAILABLE_CLI_OPTIONS from freqtrade.constants import DEFAULT_CONFIG + ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"] ARGS_STRATEGY = ["strategy", "strategy_path"] @@ -26,7 +27,7 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", "use_max_market_positions", "print_all", "print_colorized", "print_json", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", - "hyperopt_continue", "hyperopt_loss"] + "hyperopt_loss"] 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_objective", "hyperopt_list_max_objective", "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", - "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", "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._build_args(optionlist=['version'], parser=self.parser) - from freqtrade.commands import (start_create_userdir, start_convert_data, - start_download_data, start_list_data, - start_hyperopt_list, start_hyperopt_show, + from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir, + start_download_data, start_edge, start_hyperopt, + start_hyperopt_list, start_hyperopt_show, start_list_data, start_list_exchanges, start_list_hyperopts, start_list_markets, start_list_strategies, - start_list_timeframes, start_new_config, - start_new_hyperopt, start_new_strategy, - start_plot_dataframe, start_plot_profit, start_show_trades, - start_backtesting, start_hyperopt, start_edge, - start_test_pairlist, start_trading) + start_list_timeframes, start_new_config, start_new_hyperopt, + start_new_strategy, start_plot_dataframe, start_plot_profit, + start_show_trades, start_test_pairlist, start_trading) subparsers = self.parser.add_subparsers(dest='command', # Use custom message when no subhandler is added diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 0c98b2e55..7bdbcc057 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -1,13 +1,15 @@ import logging from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, List from questionary import Separator, prompt 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.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges +from freqtrade.misc import render_template + + logger = logging.getLogger(__name__) @@ -46,7 +48,7 @@ def ask_user_config() -> Dict[str, Any]: Interactive questions built using https://github.com/tmbo/questionary :returns: Dict with keys to put into template """ - questions = [ + questions: List[Dict[str, Any]] = [ { "type": "confirm", "name": "dry_run", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 81b8de1af..4769bccde 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -4,6 +4,7 @@ Definition of cli arguments used in arguments.py from argparse import ArgumentTypeError from freqtrade import __version__, constants +from freqtrade.constants import HYPEROPT_LOSS_BUILTIN def check_int_positive(value: str) -> int: @@ -252,23 +253,19 @@ AVAILABLE_CLI_OPTIONS = { metavar='INT', 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', help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' 'Different functions can generate completely different results, ' 'since the target for optimization is different. Built-in Hyperopt-loss-functions are: ' - 'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, ' - 'SortinoHyperOptLoss, SortinoHyperOptLossDaily.' - '(default: `%(default)s`).', + f'{", ".join(HYPEROPT_LOSS_BUILTIN)}', 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 "print_one_column": Arg( diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 956a8693e..df4c52de0 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -6,16 +6,15 @@ from typing import Any, Dict, List import arrow from freqtrade.configuration import TimeRange, setup_utils_configuration -from freqtrade.data.converter import (convert_ohlcv_format, - convert_trades_format) -from freqtrade.data.history import (convert_trades_to_ohlcv, - refresh_backtest_ohlcv_data, +from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format +from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, refresh_backtest_trades_data) from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode + logger = logging.getLogger(__name__) @@ -36,6 +35,9 @@ def start_download_data(args: Dict[str, Any]) -> None: if 'timerange' in config: 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: raise OperationalException( "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) - from freqtrade.data.history.idatahandler import get_datahandler from tabulate import tabulate + + from freqtrade.data.history.idatahandler import get_datahandler dhc = get_datahandler(config['datadir'], config['dataformat_ohlcv']) paircombs = dhc.ohlcv_get_available_data(config['datadir']) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index bfd68cb9b..a0105e140 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -4,13 +4,13 @@ from pathlib import Path from typing import Any, Dict from freqtrade.configuration import setup_utils_configuration -from freqtrade.configuration.directory_operations import (copy_sample_files, - create_userdata_dir) +from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES from freqtrade.exceptions import OperationalException from freqtrade.misc import render_template, render_template_with_fallback from freqtrade.state import RunMode + logger = logging.getLogger(__name__) @@ -133,7 +133,7 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: if new_path.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']) else: raise OperationalException("`new-hyperopt` requires --hyperopt to be set.") diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 4fae51e28..fd8f737f0 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -5,9 +5,11 @@ from typing import Any, Dict, List from colorama import init as colorama_init from freqtrade.configuration import setup_utils_configuration +from freqtrade.data.btanalysis import get_latest_hyperopt_file from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode + 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), } - results_file = (config['user_data_dir'] / - 'hyperopt_results' / 'hyperopt_results.pickle') + results_file = get_latest_hyperopt_file( + config['user_data_dir'] / 'hyperopt_results', + config.get('hyperoptexportfilename')) # Previous evaluations 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) no_header = config.get('hyperopt_show_no_header', False) - results_file = (config['user_data_dir'] / - 'hyperopt_results' / 'hyperopt_results.pickle') + results_file = get_latest_hyperopt_file( + config['user_data_dir'] / 'hyperopt_results', + config.get('hyperoptexportfilename')) + n = config.get('hyperopt_show_index', -1) filteroptions = { diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index c8c820c61..9e6076dfb 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -5,20 +5,20 @@ from collections import OrderedDict from pathlib import Path from typing import Any, Dict, List -from colorama import init as colorama_init -from colorama import Fore, Style import rapidjson +from colorama import Fore, Style +from colorama import init as colorama_init from tabulate import tabulate from freqtrade.configuration import setup_utils_configuration from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES from freqtrade.exceptions import OperationalException -from freqtrade.exchange import (available_exchanges, ccxt_exchanges, - market_is_active) +from freqtrade.exchange import available_exchanges, ccxt_exchanges, market_is_active from freqtrade.misc import plural from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode + logger = logging.getLogger(__name__) @@ -203,15 +203,16 @@ def start_show_trades(args: Dict[str, Any]) -> None: """ Show trades """ - from freqtrade.persistence import init, Trade import json + + from freqtrade.persistence import Trade, init_db config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) if 'db_url' not in config: raise OperationalException("--db-url is required for this command.") 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 = [] if config.get('trade_ids'): diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index 2fc605926..7411ca9c6 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -6,6 +6,7 @@ from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.state import RunMode + 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 try: from filelock import FileLock, Timeout + from freqtrade.optimize.hyperopt import Hyperopt except ImportError as e: raise OperationalException( @@ -98,6 +100,7 @@ def start_edge(args: Dict[str, Any]) -> None: :return: None """ from freqtrade.optimize.edge_cli import EdgeCli + # Initialize configuration config = setup_optimize_configuration(args, RunMode.EDGE) logger.info('Starting freqtrade in Edge mode') diff --git a/freqtrade/commands/pairlist_commands.py b/freqtrade/commands/pairlist_commands.py index 77bcb04b4..e4ee80ca5 100644 --- a/freqtrade/commands/pairlist_commands.py +++ b/freqtrade/commands/pairlist_commands.py @@ -7,6 +7,7 @@ from freqtrade.configuration import setup_utils_configuration from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode + logger = logging.getLogger(__name__) diff --git a/freqtrade/commands/trade_commands.py b/freqtrade/commands/trade_commands.py index c058e4f9d..535844844 100644 --- a/freqtrade/commands/trade_commands.py +++ b/freqtrade/commands/trade_commands.py @@ -1,5 +1,4 @@ import logging - from typing import Any, Dict diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index d41ac97ec..607f9cdef 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,7 +1,7 @@ # 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.timerange import TimeRange -from freqtrade.configuration.configuration import Configuration +from freqtrade.configuration.config_setup import setup_utils_configuration from freqtrade.configuration.config_validation import validate_config_consistency +from freqtrade.configuration.configuration import Configuration +from freqtrade.configuration.timerange import TimeRange diff --git a/freqtrade/configuration/check_exchange.py b/freqtrade/configuration/check_exchange.py index 92daaf251..aa36de3ff 100644 --- a/freqtrade/configuration/check_exchange.py +++ b/freqtrade/configuration/check_exchange.py @@ -2,11 +2,11 @@ import logging from typing import Any, Dict from freqtrade.exceptions import OperationalException -from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason, - is_exchange_bad, is_exchange_known_ccxt, - is_exchange_officially_supported) +from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason, is_exchange_bad, + is_exchange_known_ccxt, is_exchange_officially_supported) from freqtrade.state import RunMode + logger = logging.getLogger(__name__) diff --git a/freqtrade/configuration/config_setup.py b/freqtrade/configuration/config_setup.py index 64f283e42..3b0f778f4 100644 --- a/freqtrade/configuration/config_setup.py +++ b/freqtrade/configuration/config_setup.py @@ -1,10 +1,12 @@ import logging from typing import Any, Dict +from freqtrade.state import RunMode + +from .check_exchange import remove_credentials from .config_validation import validate_config_consistency from .configuration import Configuration -from .check_exchange import remove_credentials -from freqtrade.state import RunMode + logger = logging.getLogger(__name__) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 5ba7ff294..d4612d8e0 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -9,6 +9,7 @@ from freqtrade import constants from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode + logger = logging.getLogger(__name__) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 930917fae..1ca3187fb 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -10,14 +10,14 @@ from typing import Any, Callable, Dict, List, Optional from freqtrade import constants from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings -from freqtrade.configuration.directory_operations import (create_datadir, - create_userdata_dir) +from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir from freqtrade.configuration.load_config import load_config_file from freqtrade.exceptions import OperationalException from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts, json_load from freqtrade.state import NON_UTIL_MODES, TRADING_MODES, RunMode + logger = logging.getLogger(__name__) @@ -263,6 +263,9 @@ class Configuration: self._args_to_config(config, argname='hyperopt_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', logstring='Parameter --epochs detected ... ' 'Will run Hyperopt with for {} epochs ...' @@ -295,9 +298,6 @@ class Configuration: self._args_to_config(config, argname='hyperopt_min_trades', logstring='Parameter --min-trades detected: {}') - self._args_to_config(config, argname='hyperopt_continue', - logstring='Hyperopt continue: {}') - self._args_to_config(config, argname='hyperopt_loss', logstring='Using Hyperopt loss class name: {}') diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index 6b8c8cb5a..51310f013 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -3,8 +3,9 @@ import shutil from pathlib import Path from typing import Any, Dict, Optional -from freqtrade.exceptions import OperationalException from freqtrade.constants import USER_DATA_FILES +from freqtrade.exceptions import OperationalException + logger = logging.getLogger(__name__) diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index a24ee3d0a..726126034 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -11,6 +11,7 @@ import rapidjson from freqtrade.exceptions import OperationalException + logger = logging.getLogger(__name__) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index de663bd4b..dc5384f6f 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -11,7 +11,6 @@ DEFAULT_EXCHANGE = 'bittrex' PROCESS_THROTTLE_SECS = 5 # sec HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec -DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite' UNLIMITED_STAKE_AMOUNT = 'unlimited' @@ -21,6 +20,9 @@ REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] ORDERBOOK_SIDES = ['ask', 'bid'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] +HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', + 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', + 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PrecisionFilter', 'PriceFilter', 'ShuffleFilter', 'SpreadFilter'] diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 2d45a7222..513fba9e7 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -2,17 +2,17 @@ Helpers when analyzing backtest data """ import logging +from datetime import timezone 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 pandas as pd -from datetime import timezone -from freqtrade import persistence from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.misc import json_load -from freqtrade.persistence import Trade +from freqtrade.persistence import Trade, init_db + 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"] -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'. :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 :raises: ValueError in the following cases: * Directory does not exist @@ -44,10 +45,57 @@ def get_latest_backtest_filename(directory: Union[Path, str]) -> str: with filename.open() as 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.") - 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]: @@ -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. :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", "open_rate", "close_rate", "amount", "trade_duration", "sell_reason", diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 100a578a2..38fa670e9 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -10,8 +10,8 @@ from typing import Any, Dict, List import pandas as pd from pandas import DataFrame, to_datetime -from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, - DEFAULT_TRADES_COLUMNS) +from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS + logger = logging.getLogger(__name__) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index ccb6cbf56..07dd94fc1 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -17,6 +17,7 @@ from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.exchange import Exchange from freqtrade.state import RunMode + logger = logging.getLogger(__name__) diff --git a/freqtrade/data/history/__init__.py b/freqtrade/data/history/__init__.py index 23f635a98..107f9c401 100644 --- a/freqtrade/data/history/__init__.py +++ b/freqtrade/data/history/__init__.py @@ -5,10 +5,8 @@ Includes: * load data for a pair (or a list of pairs) from disk * download data from exchange and store to disk """ - -from .history_utils import (convert_trades_to_ohlcv, # noqa: F401 - get_timerange, load_data, load_pair_history, - refresh_backtest_ohlcv_data, - refresh_backtest_trades_data, refresh_data, +# flake8: noqa: F401 +from .history_utils import (convert_trades_to_ohlcv, get_timerange, load_data, load_pair_history, + refresh_backtest_ohlcv_data, refresh_backtest_trades_data, refresh_data, validate_backtest_data) -from .idatahandler import get_datahandler # noqa: F401 +from .idatahandler import get_datahandler diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index 594a1598a..f6cf9e0d9 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -7,12 +7,12 @@ import pandas as pd from freqtrade import misc from freqtrade.configuration import TimeRange -from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, - DEFAULT_TRADES_COLUMNS, +from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, ListPairsWithTimeframes) from .idatahandler import IDataHandler, TradeList + logger = logging.getLogger(__name__) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index ac234a72e..a420b9dcc 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -9,15 +9,14 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS -from freqtrade.data.converter import (clean_ohlcv_dataframe, - ohlcv_to_dataframe, - trades_remove_duplicates, - trades_to_ohlcv) +from freqtrade.data.converter import (clean_ohlcv_dataframe, ohlcv_to_dataframe, + trades_remove_duplicates, trades_to_ohlcv) from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler from freqtrade.exceptions import OperationalException from freqtrade.exchange import Exchange from freqtrade.misc import format_ms_time + logger = logging.getLogger(__name__) diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index 01b14f501..a170a9dc5 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -14,10 +14,10 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.constants import ListPairsWithTimeframes -from freqtrade.data.converter import (clean_ohlcv_dataframe, - trades_remove_duplicates, trim_dataframe) +from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe from freqtrade.exchange import timeframe_to_seconds + logger = logging.getLogger(__name__) # Type for trades list diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index 2e7c0f773..6436aa13d 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -8,12 +8,12 @@ from pandas import DataFrame, read_json, to_datetime from freqtrade import misc from freqtrade.configuration import TimeRange -from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, - ListPairsWithTimeframes) +from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, ListPairsWithTimeframes from freqtrade.data.converter import trades_dict_to_list from .idatahandler import IDataHandler, TradeList + logger = logging.getLogger(__name__) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 3213c1ef8..a40b63d67 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -9,11 +9,12 @@ import utils_find_1st as utf1st from pandas import DataFrame from freqtrade.configuration import TimeRange -from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, DATETIME_PRINT_FORMAT -from freqtrade.exceptions import OperationalException +from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT from freqtrade.data.history import get_timerange, load_data, refresh_data +from freqtrade.exceptions import OperationalException from freqtrade.strategy.interface import SellType + logger = logging.getLogger(__name__) @@ -309,8 +310,10 @@ class Edge: # Calculating number of losing trades, average win and average loss df['nb_loss_trades'] = df['nb_trades'] - df['nb_win_trades'] - df['average_win'] = df['profit_sum'] / df['nb_win_trades'] - df['average_loss'] = df['loss_sum'] / df['nb_loss_trades'] + df['average_win'] = np.where(df['nb_win_trades'] == 0, 0.0, + 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 df['winrate'] = df['nb_win_trades'] / df['nb_trades'] diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index bdf1f91ec..5b58d7a95 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,19 +1,16 @@ # flake8: noqa: F401 +# isort: off from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS from freqtrade.exchange.exchange import Exchange -from freqtrade.exchange.exchange import (get_exchange_bad_reason, - 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 +# isort: on 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.kraken import Kraken diff --git a/freqtrade/exchange/bibox.py b/freqtrade/exchange/bibox.py index 229abe766..f0c2dd00b 100644 --- a/freqtrade/exchange/bibox.py +++ b/freqtrade/exchange/bibox.py @@ -4,6 +4,7 @@ from typing import Dict from freqtrade.exchange import Exchange + logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index d7da34482..099f282a2 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -4,12 +4,12 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, - InvalidOrderException, OperationalException, - TemporaryError) +from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, + OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier + logger = logging.getLogger(__name__) @@ -20,20 +20,9 @@ class Binance(Exchange): "order_time_in_force": ['gtc', 'fok', 'ioc'], "trades_pagination": "id", "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: """ Verify stop_loss against stoploss-order value (limit or price) diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py new file mode 100644 index 000000000..4318f9cf0 --- /dev/null +++ b/freqtrade/exchange/bittrex.py @@ -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], + } diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 9abd42aa7..ce0fde9e4 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -3,8 +3,8 @@ import logging import time from functools import wraps -from freqtrade.exceptions import (DDosProtection, RetryableOrderError, - TemporaryError) +from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError + logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index aac45967d..da3c83b0c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -13,20 +13,20 @@ from typing import Any, Dict, List, Optional, Tuple import arrow import ccxt import ccxt.async_support as ccxt_async -from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, - TRUNCATE, decimal_to_precision) +from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, + decimal_to_precision) from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list -from freqtrade.exceptions import (DDosProtection, ExchangeError, - InsufficientFundsError, - InvalidOrderException, OperationalException, - RetryableOrderError, TemporaryError) -from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, - BAD_EXCHANGES, retrier, retrier_async) +from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, + InvalidOrderException, OperationalException, RetryableOrderError, + TemporaryError) +from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, retrier, + retrier_async) from freqtrade.misc import deep_merge_dicts, safe_value_fallback2 + CcxtModuleType = Any @@ -53,7 +53,7 @@ class Exchange: "ohlcv_partial_candle": True, "trades_pagination": "time", # Possible are "time" or "id" "trades_pagination_arg": "since", - + "l2_limit_range": None, } _ft_has: Dict = {} @@ -687,6 +687,9 @@ class Exchange: async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int) -> List: + """ + Download historic ohlcv + """ one_call = timeframe_to_msecs(timeframe) * self._ohlcv_candle_limit logger.debug( @@ -702,9 +705,14 @@ class Exchange: # Combine gathered results 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: - data.extend(res) + data.extend(new_data) # Sort data again after extending the result - above calls return in "async order" data = sorted(data, key=lambda x: x[0]) logger.info("Downloaded data for %s with length %s.", pair, len(data)) @@ -741,9 +749,8 @@ class Exchange: if isinstance(res, Exception): logger.warning("Async code raised an exception: %s", res.__class__.__name__) continue - pair = res[0] - timeframe = res[1] - ticks = res[2] + # Deconstruct tuple (has 3 elements) + pair, timeframe, ticks = res # keeping last candle time as last refreshed time of the pair if ticks: 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_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 def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: """ @@ -1077,9 +1094,10 @@ class Exchange: Returns a dict in the format {'asks': [price, volume], 'bids': [price, volume]} """ + limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range']) try: - return self._api.fetch_l2_order_book(pair, limit) + return self._api.fetch_l2_order_book(pair, limit1) except ccxt.NotSupported as e: raise OperationalException( f'Exchange {self._api.name} does not support fetching order book.' diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index a5ee0c408..f05490cbb 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -4,12 +4,12 @@ from typing import Any, Dict import ccxt -from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, - InvalidOrderException, OperationalException, - TemporaryError) +from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, + OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier + logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index e6b5da88e..5b7aa5c5b 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -4,12 +4,12 @@ from typing import Any, Dict import ccxt -from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, - InvalidOrderException, OperationalException, - TemporaryError) +from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, + OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier + logger = logging.getLogger(__name__) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index eec09a17c..7416d8236 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging import traceback -from datetime import datetime +from datetime import datetime, timezone from math import isclose from threading import Lock from typing import Any, Dict, List, Optional @@ -12,17 +12,17 @@ from typing import Any, Dict, List, Optional import arrow from cachetools import TTLCache -from freqtrade import __version__, constants, persistence +from freqtrade import __version__, constants from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, 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.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.rpc import RPCManager, RPCMessageType 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.wallets import Wallets + logger = logging.getLogger(__name__) @@ -57,8 +58,8 @@ class FreqtradeBot: # 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 # refreshed once every iteration. - self._sell_rate_cache = TTLCache(maxsize=100, ttl=1800) - self._buy_rate_cache = TTLCache(maxsize=100, ttl=1800) + self._sell_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) + self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) 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) - 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) + PairLocks.timeframe = self.config['timeframe'] + self.pairlists = PairListManager(self.exchange, self.config) self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) @@ -122,7 +125,7 @@ class FreqtradeBot: self.check_for_open_trades() self.rpc.cleanup() - persistence.cleanup() + cleanup_db() def startup(self) -> None: """ @@ -344,27 +347,27 @@ class FreqtradeBot: whitelist = copy.deepcopy(self.active_pair_whitelist) if not whitelist: logger.info("Active pair whitelist is empty.") - else: - # Remove pairs for currently opened trades from the whitelist - for trade in Trade.get_open_trades(): - if trade.pair in whitelist: - whitelist.remove(trade.pair) - logger.debug('Ignoring %s in pair whitelist', trade.pair) + return trades_created + # Remove pairs for currently opened trades from the whitelist + for trade in Trade.get_open_trades(): + if trade.pair in whitelist: + whitelist.remove(trade.pair) + logger.debug('Ignoring %s in pair whitelist', trade.pair) - if not whitelist: - logger.info("No currency pair in active pair whitelist, " - "but checking to sell open trades.") - else: - # Create entity and execute trade for each pair from whitelist - for pair in whitelist: - try: - trades_created += self.create_trade(pair) - except DependencyException as exception: - logger.warning('Unable to create trade for %s: %s', pair, exception) + if not whitelist: + logger.info("No currency pair in active pair whitelist, " + "but checking to sell open trades.") + return trades_created + # Create entity and execute trade for each pair from whitelist + for pair in whitelist: + try: + trades_created += self.create_trade(pair) + except DependencyException as exception: + logger.warning('Unable to create trade for %s: %s', pair, exception) - if not trades_created: - logger.debug("Found no buy signals for whitelisted currencies. " - "Trying again...") + if not trades_created: + logger.debug("Found no buy signals for whitelisted currencies. " + "Trying again...") return trades_created @@ -936,8 +939,8 @@ class FreqtradeBot: self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, stoploss_order=True) # 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, "stoploss") return True @@ -1263,7 +1266,8 @@ class FreqtradeBot: Trade.session.flush() # 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) diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index 8f5da9bee..169cd2610 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -1,12 +1,12 @@ import logging import sys from logging import Formatter -from logging.handlers import (BufferingHandler, RotatingFileHandler, - SysLogHandler) +from logging.handlers import BufferingHandler, RotatingFileHandler, SysLogHandler from typing import Any, Dict from freqtrade.exceptions import OperationalException + logger = logging.getLogger(__name__) LOGFORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' diff --git a/freqtrade/main.py b/freqtrade/main.py index dc26c2a46..5f8d5d19d 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -7,6 +7,7 @@ import logging import sys from typing import Any, List + # check min. python version if sys.version_info < (3, 6): sys.exit("Freqtrade requires Python version >= 3.6") diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 623f6cb8f..359d0d0e4 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -12,6 +12,7 @@ from typing.io import IO import numpy as np import rapidjson + logger = logging.getLogger(__name__) @@ -41,7 +42,7 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray: 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 :param filename: file to create @@ -52,12 +53,14 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False) -> None: if is_zip: if filename.suffix != '.gz': filename = filename.with_suffix('.gz') - logger.info(f'dumping json to "{filename}"') + if log: + logger.info(f'dumping json to "{filename}"') - with gzip.open(filename, 'w') as fp: - rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE) + with gzip.open(filename, 'w') as fpz: + rapidjson.dump(data, fpz, default=str, number_mode=rapidjson.NM_NATIVE) else: - logger.info(f'dumping json to "{filename}"') + if log: + logger.info(f'dumping json to "{filename}"') with open(filename, 'w') as fp: rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8d4a3a205..47bb9edd9 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -4,31 +4,39 @@ This module contains the backtesting logic """ import logging +from collections import defaultdict from copy import deepcopy from datetime import datetime, timedelta from typing import Any, Dict, List, NamedTuple, Optional, Tuple -import arrow from pandas import DataFrame -from freqtrade.configuration import (TimeRange, remove_credentials, - validate_config_consistency) +from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.data import history from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds -from freqtrade.optimize.optimize_reports import (generate_backtest_stats, - show_backtest_results, +from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType + 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): """ @@ -116,7 +124,7 @@ class Backtesting: """ Load strategy into backtesting """ - self.strategy = strategy + self.strategy: IStrategy = strategy # Set stoploss_on_exchange to false for backtesting, # since a "perfect" stoploss-sell is assumed anyway # And the regular "stoploss" function would not apply to that case @@ -148,12 +156,14 @@ class Backtesting: 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. 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'] data: Dict = {} # Create dict with data @@ -173,10 +183,10 @@ class Backtesting: # Convert from Pandas to list for performance reasons # (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 - 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: """ Get close rate for backtesting result @@ -187,12 +197,12 @@ class Backtesting: return trade.stop_loss elif sell.sell_type == (SellType.ROI): 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: # 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) # - 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) close_rate = - (trade.open_rate * roi + trade.open_rate * @@ -200,91 +210,79 @@ class Backtesting: if (trade_dur > 0 and trade_dur == roi_entry 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. # 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 # cannot sell outside of a candle. # 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: # This should not be reached... - return sell_row.open + return sell_row[OPEN_IDX] else: - return sell_row.open + return sell_row[OPEN_IDX] - def _get_sell_trade_entry( - self, pair: str, buy_row: DataFrame, - partial_ohlcv: List, trade_count_lock: Dict, - stake_amount: float, max_open_trades: int) -> Optional[BacktestResult]: + def _get_sell_trade_entry(self, trade: Trade, sell_row: Tuple) -> Optional[BacktestResult]: - trade = Trade( - pair=pair, - open_rate=buy_row.open, - 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_IDX], sell_row[DATE_IDX], + sell_row[BUY_IDX], sell_row[SELL_IDX], + low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) + if sell.sell_flag: + trade_dur = int((sell_row[DATE_IDX] - trade.open_date).total_seconds() // 60) + closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) - 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: - trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60) - closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) - - return BacktestResult(pair=pair, - profit_percent=trade.calc_profit_ratio(rate=closerate), - profit_abs=trade.calc_profit(rate=closerate), - open_date=buy_row.date, - open_rate=buy_row.open, - open_fee=self.fee, - close_date=sell_row.date, - close_rate=closerate, - close_fee=self.fee, - amount=trade.amount, - trade_duration=trade_dur, - open_at_end=False, - sell_reason=sell.sell_type - ) - if partial_ohlcv: - # no sell condition found - trade stil open at end of backtest period - sell_row = partial_ohlcv[-1] - bt_res = BacktestResult(pair=pair, - profit_percent=trade.calc_profit_ratio(rate=sell_row.open), - profit_abs=trade.calc_profit(rate=sell_row.open), - open_date=buy_row.date, - open_rate=buy_row.open, - open_fee=self.fee, - close_date=sell_row.date, - close_rate=sell_row.open, - close_fee=self.fee, - amount=trade.amount, - trade_duration=int(( - sell_row.date - buy_row.date).total_seconds() // 60), - open_at_end=True, - sell_reason=SellType.FORCE_SELL - ) - logger.debug(f"{pair} - Force selling still open trade, " - f"profit percent: {bt_res.profit_percent}, " - f"profit abs: {bt_res.profit_abs}") - - return bt_res + return BacktestResult(pair=trade.pair, + profit_percent=trade.calc_profit_ratio(rate=closerate), + profit_abs=trade.calc_profit(rate=closerate), + open_date=trade.open_date, + open_rate=trade.open_rate, + open_fee=self.fee, + close_date=sell_row[DATE_IDX], + close_rate=closerate, + close_fee=self.fee, + amount=trade.amount, + trade_duration=trade_dur, + open_at_end=False, + sell_reason=sell.sell_type + ) return None + def handle_left_open(self, open_trades: Dict[str, List[Trade]], + data: Dict[str, List[Tuple]]) -> List[BacktestResult]: + """ + Handling of left open trades at the end of backtesting + """ + 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, + close_date=sell_row[DATE_IDX], + close_rate=sell_row[OPEN_IDX], + close_fee=self.fee, + amount=trade.amount, + trade_duration=int(( + sell_row[DATE_IDX] - trade.open_date + ).total_seconds() // 60), + open_at_end=True, + sell_reason=SellType.FORCE_SELL + ) + trades.append(trade_entry) + return trades + 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: """ Implement backtesting functionality @@ -306,19 +304,21 @@ class Backtesting: f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}" ) trades = [] - trade_count_lock: Dict = {} # Use dict of lists with data for performance # (looping lists is a lot faster than pandas DataFrames) 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: Dict = {} 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 - while tmp < end_date: + while tmp <= end_date: + open_trade_count_start = open_trade_count for i, pair in enumerate(data): if pair not in indexes: @@ -332,42 +332,52 @@ class Backtesting: continue # 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 - indexes[pair] += 1 - if row.buy == 0 or row.sell == 1: - continue # skip rows where no buy signal or that would immediately sell off + # without positionstacking, we can only have one open trade per pair. + # max_open_trades must be respected + # don't open on the last row + if ((position_stacking or len(open_trades[pair]) == 0) + and max_open_trades > 0 and open_trade_count_start < max_open_trades + and tmp != end_date + and row[BUY_IDX] == 1 and row[SELL_IDX] != 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) - 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. - continue - - if max_open_trades > 0: - # Check if max_open_trades has already been reached for the given date - if not trade_count_lock.get(row.date, 0) < max_open_trades: - continue - trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 - - # since indexes has been incremented before, we need to go one step back to - # also check the buying candle for sell conditions. - trade_entry = self._get_sell_trade_entry(pair, row, data[pair][indexes[pair]-1:], - trade_count_lock, stake_amount, - max_open_trades) - - if trade_entry: - logger.debug(f"{pair} - Locking pair till " - f"close_date={trade_entry.close_date}") - lock_pair_until[pair] = trade_entry.close_date - 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 + for trade in open_trades[pair]: + # since indexes has been incremented before, we need to go one step back to + # also check the buying candle for sell conditions. + trade_entry = self._get_sell_trade_entry(trade, row) + # Sell occured + if trade_entry: + # logger.debug(f"{pair} - Backtesting sell {trade}") + open_trade_count -= 1 + open_trades[pair].remove(trade) + trades.append(trade_entry) # Move time one configured time_interval ahead. tmp += timedelta(minutes=self.timeframe_min) + + trades += self.handle_left_open(open_trades, data=data) + return DataFrame.from_records(trades, columns=BacktestResult._fields) def start(self) -> None: @@ -413,8 +423,8 @@ class Backtesting: results = self.backtest( processed=preprocessed, stake_amount=self.config['stake_amount'], - start_date=min_date, - end_date=max_date, + start_date=min_date.datetime, + end_date=max_date.datetime, max_open_trades=max_open_trades, position_stacking=position_stacking, ) diff --git a/freqtrade/optimize/default_hyperopt_loss.py b/freqtrade/optimize/default_hyperopt_loss.py index 9e780d0ea..9dbdc4403 100644 --- a/freqtrade/optimize/default_hyperopt_loss.py +++ b/freqtrade/optimize/default_hyperopt_loss.py @@ -1,5 +1,5 @@ """ -DefaultHyperOptLoss +ShortTradeDurHyperOptLoss This module defines the default HyperoptLoss class which is being used for Hyperoptimization. """ @@ -26,7 +26,7 @@ EXPECTED_MAX_PROFIT = 3.0 MAX_ACCEPTED_TRADE_DURATION = 300 -class DefaultHyperOptLoss(IHyperOptLoss): +class ShortTradeDurHyperOptLoss(IHyperOptLoss): """ 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) result = trade_loss + profit_loss + duration_loss return result + + +# Create an alias for This to allow the legacy Method to work as well. +DefaultHyperOptLoss = ShortTradeDurHyperOptLoss diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index be19688d8..a5f505bee 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -7,12 +7,12 @@ import logging from typing import Any, Dict from freqtrade import constants -from freqtrade.configuration import (TimeRange, remove_credentials, - validate_config_consistency) +from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency from freqtrade.edge import Edge from freqtrade.optimize.optimize_reports import generate_edge_table from freqtrade.resolvers import ExchangeResolver, StrategyResolver + logger = logging.getLogger(__name__) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 37de3bc4b..7870ba1cf 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -10,6 +10,7 @@ import logging import random import warnings from collections import OrderedDict +from datetime import datetime from math import ceil from operator import itemgetter from pathlib import Path @@ -21,24 +22,22 @@ import rapidjson import tabulate from colorama import Fore, Style from colorama import init as colorama_init -from joblib import (Parallel, cpu_count, delayed, dump, load, - wrap_non_picklable_objects) +from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects 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.history import get_timerange 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 # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 -from freqtrade.optimize.hyperopt_loss_interface import \ - IHyperOptLoss # noqa: F401 -from freqtrade.resolvers.hyperopt_resolver import (HyperOptLossResolver, - HyperOptResolver) +from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 +from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver from freqtrade.strategy import IStrategy + # Suppress scikit-learn FutureWarnings from skopt with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) @@ -77,19 +76,16 @@ class Hyperopt: self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config) 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'] / - 'hyperopt_results' / 'hyperopt_results.pickle') + 'hyperopt_results' / f'hyperopt_results_{time_now}.pickle') self.data_pickle_file = (self.config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_tickerdata.pkl') self.total_epochs = config.get('epochs', 0) self.current_best_loss = 100 - if not self.config.get('hyperopt_continue'): - self.clean_hyperopt() - else: - logger.info("Continuing on previous hyperopt results.") + self.clean_hyperopt() 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) if hasattr(self.custom_hyperopt, 'populate_indicators'): - self.backtesting.strategy.advise_indicators = \ - self.custom_hyperopt.populate_indicators # type: ignore + self.backtesting.strategy.advise_indicators = ( # type: ignore + self.custom_hyperopt.populate_indicators) # type: ignore if hasattr(self.custom_hyperopt, 'populate_buy_trend'): - self.backtesting.strategy.advise_buy = \ - self.custom_hyperopt.populate_buy_trend # type: ignore + self.backtesting.strategy.advise_buy = ( # type: ignore + self.custom_hyperopt.populate_buy_trend) # type: ignore if hasattr(self.custom_hyperopt, 'populate_sell_trend'): - self.backtesting.strategy.advise_sell = \ - self.custom_hyperopt.populate_sell_trend # type: ignore + self.backtesting.strategy.advise_sell = ( # 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 if self.config.get('use_max_market_positions', True): @@ -165,6 +161,10 @@ class Hyperopt: self.num_epochs_saved = num_epochs logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} " 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 def _read_results(results_file: Path) -> List: @@ -262,6 +262,11 @@ class Hyperopt: ), default=str, indent=4, number_mode=rapidjson.NM_NATIVE) 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: params_result += f"{space}_params = {pformat(space_params, indent=4)}" params_result = params_result.replace("}", "\n}").replace("{", "{\n ") @@ -503,16 +508,16 @@ class Hyperopt: params_details = self._get_params_details(params_dict) if self.has_space('roi'): - self.backtesting.strategy.minimal_roi = \ - self.custom_hyperopt.generate_roi_table(params_dict) + self.backtesting.strategy.minimal_roi = ( # type: ignore + self.custom_hyperopt.generate_roi_table(params_dict)) if self.has_space('buy'): - self.backtesting.strategy.advise_buy = \ - self.custom_hyperopt.buy_strategy_generator(params_dict) + self.backtesting.strategy.advise_buy = ( # type: ignore + self.custom_hyperopt.buy_strategy_generator(params_dict)) if self.has_space('sell'): - self.backtesting.strategy.advise_sell = \ - self.custom_hyperopt.sell_strategy_generator(params_dict) + self.backtesting.strategy.advise_sell = ( # type: ignore + self.custom_hyperopt.sell_strategy_generator(params_dict)) if self.has_space('stoploss'): self.backtesting.strategy.stoploss = params_dict['stoploss'] @@ -533,8 +538,8 @@ class Hyperopt: backtesting_results = self.backtesting.backtest( processed=processed, stake_amount=self.config['stake_amount'], - start_date=min_date, - end_date=max_date, + start_date=min_date.datetime, + end_date=max_date.datetime, max_open_trades=self.max_open_trades, position_stacking=self.position_stacking, ) @@ -657,8 +662,6 @@ class Hyperopt: self.backtesting.strategy.dp = None # type: ignore IStrategy.dp = None # type: ignore - self.epochs = self.load_previous_results(self.results_file) - cpus = cpu_count() logger.info(f"Found {cpus} CPU cores. Let's make them scream!") config_jobs = self.config.get('hyperopt_jobs', -1) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 65069b984..b8c44ed59 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -13,6 +13,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict + logger = logging.getLogger(__name__) diff --git a/freqtrade/optimize/hyperopt_loss_sharpe.py b/freqtrade/optimize/hyperopt_loss_sharpe.py index 29377bdd5..232fb33b6 100644 --- a/freqtrade/optimize/hyperopt_loss_sharpe.py +++ b/freqtrade/optimize/hyperopt_loss_sharpe.py @@ -6,8 +6,8 @@ Hyperoptimization. """ from datetime import datetime -from pandas import DataFrame import numpy as np +from pandas import DataFrame from freqtrade.optimize.hyperopt import IHyperOptLoss diff --git a/freqtrade/optimize/hyperopt_loss_sortino.py b/freqtrade/optimize/hyperopt_loss_sortino.py index d470a9977..c0ff0773a 100644 --- a/freqtrade/optimize/hyperopt_loss_sortino.py +++ b/freqtrade/optimize/hyperopt_loss_sortino.py @@ -6,8 +6,8 @@ Hyperoptimization. """ from datetime import datetime -from pandas import DataFrame import numpy as np +from pandas import DataFrame from freqtrade.optimize.hyperopt import IHyperOptLoss diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 696e63b25..3db9a312a 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -4,14 +4,15 @@ from pathlib import Path from typing import Any, Dict, List, Union from arrow import Arrow -from pandas import DataFrame from numpy import int64 +from pandas import DataFrame from tabulate import tabulate 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 + logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index 64f01cb61..19cf1c090 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -2,9 +2,10 @@ Minimum age (days listed) pair list filter """ import logging -import arrow from typing import Any, Dict +import arrow + from freqtrade.exceptions import OperationalException from freqtrade.misc import plural from freqtrade.pairlist.IPairList import IPairList diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 67a96cc60..6b5bd11e7 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -36,7 +36,7 @@ class IPairList(ABC): self._pairlist_pos = pairlist_pos self.refresh_period = self._pairlistconfig.get('refresh_period', 1800) 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 def name(self) -> str: diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index 3061d3d01..cf853397b 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -4,8 +4,9 @@ Precision pair list filter import logging from typing import Any, Dict -from freqtrade.pairlist.IPairList import IPairList from freqtrade.exceptions import OperationalException +from freqtrade.pairlist.IPairList import IPairList + logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index 81e52768e..89bab99be 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -7,10 +7,10 @@ from typing import Dict, List from cachetools import TTLCache, cached +from freqtrade.constants import ListPairsWithTimeframes from freqtrade.exceptions import OperationalException from freqtrade.pairlist.IPairList import IPairList from freqtrade.resolvers import PairListResolver -from freqtrade.constants import ListPairsWithTimeframes logger = logging.getLogger(__name__) diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index 764856f2b..35f2bc406 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,4 +1,4 @@ # flake8: noqa: F401 -from freqtrade.persistence.models import (Order, Trade, clean_dry_run_db, - cleanup, init) +from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup_db, init_db +from freqtrade.persistence.pairlock_middleware import PairLocks diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 5089953b2..84f3ed7e6 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -3,6 +3,7 @@ from typing import List from sqlalchemy import inspect + logger = logging.getLogger(__name__) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 816e23fd3..7e6d967c1 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -7,8 +7,8 @@ from decimal import Decimal from typing import Any, Dict, List, Optional import arrow -from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, - String, create_engine, desc, func, inspect) +from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String, + create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import Query, relationship @@ -17,10 +17,12 @@ from sqlalchemy.orm.session import sessionmaker from sqlalchemy.pool import StaticPool from sqlalchemy.sql.schema import UniqueConstraint +from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate + 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' -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, 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 Order.session = Trade.session Order.query = Order.session.query_property() + PairLock.session = Trade.session + PairLock.query = PairLock.session.query_property() + previous_tables = inspect(engine).get_table_names() _DECL_BASE.metadata.create_all(engine) 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() -def cleanup() -> None: +def cleanup_db() -> None: """ Flushes all pending operations to disk. :return: None @@ -166,12 +171,12 @@ class Order(_DECL_BASE): """ 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: oobj = filtered_orders[0] oobj.update_from_ccxt_object(order) else: - logger.warning(f"Did not find order for {order['id']}.") + logger.warning(f"Did not find order for {order}.") @staticmethod 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() 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}, ' 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, '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_rate': self.open_rate, 'open_rate_requested': self.open_rate_requested, @@ -284,7 +289,7 @@ class Trade(_DECL_BASE): 'close_date_hum': (arrow.get(self.close_date).humanize() 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), 'close_timestamp': int(self.close_date.replace( 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_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, '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), 'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace( tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None, @@ -398,7 +403,7 @@ class Trade(_DECL_BASE): self.close(order['average']) else: raise ValueError(f'Unknown order type: {order_type}') - cleanup() + cleanup_db() def close(self, rate: float) -> None: """ @@ -653,3 +658,56 @@ class Trade(_DECL_BASE): trade.stop_loss = None trade.adjust_stop_loss(trade.open_rate, desired_stoploss) 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, + } diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py new file mode 100644 index 000000000..44fc228f6 --- /dev/null +++ b/freqtrade/persistence/pairlock_middleware.py @@ -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) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 270fe615b..a89732df5 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -5,11 +5,8 @@ from typing import Any, Dict, List import pandas as pd from freqtrade.configuration import TimeRange -from freqtrade.data.btanalysis import (calculate_max_drawdown, - combine_dataframes_with_mean, - create_cum_profit, - extract_trades_of_period, - load_trades) +from freqtrade.data.btanalysis import (calculate_max_drawdown, combine_dataframes_with_mean, + create_cum_profit, extract_trades_of_period, load_trades) from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider 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.strategy import IStrategy + logger = logging.getLogger(__name__) try: - from plotly.subplots import make_subplots - from plotly.offline import plot import plotly.graph_objects as go + from plotly.offline import plot + from plotly.subplots import make_subplots except ImportError: logger.exception("Module plotly not found \n Please install using `pip3 install plotly`") exit(1) diff --git a/freqtrade/resolvers/__init__.py b/freqtrade/resolvers/__init__.py index 8f79349fe..b42ec4931 100644 --- a/freqtrade/resolvers/__init__.py +++ b/freqtrade/resolvers/__init__.py @@ -1,6 +1,12 @@ -from freqtrade.resolvers.iresolver import IResolver # noqa: F401 -from freqtrade.resolvers.exchange_resolver import ExchangeResolver # noqa: F401 +# flake8: 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 -# from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver # noqa: F401 -from freqtrade.resolvers.pairlist_resolver import PairListResolver # noqa: F401 -from freqtrade.resolvers.strategy_resolver import StrategyResolver # noqa: F401 +# from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver +from freqtrade.resolvers.pairlist_resolver import PairListResolver +from freqtrade.resolvers.strategy_resolver import StrategyResolver + + + diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 2b6a731a9..ed6715d15 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -3,10 +3,11 @@ This module loads custom exchanges """ import logging -from freqtrade.exchange import Exchange, MAP_EXCHANGE_CHILDCLASS import freqtrade.exchange as exchanges +from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, Exchange from freqtrade.resolvers import IResolver + logger = logging.getLogger(__name__) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 5dcf73d67..8327a4d13 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -7,12 +7,13 @@ import logging from pathlib import Path 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.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss from freqtrade.resolvers import IResolver + logger = logging.getLogger(__name__) @@ -69,10 +70,13 @@ class HyperOptLossResolver(IResolver): :param config: configuration dictionary """ - # Verify the hyperopt_loss is in the configuration, otherwise fallback to the - # default hyperopt loss - hyperoptloss_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS - + hyperoptloss_name = config.get('hyperopt_loss') + if not hyperoptloss_name: + 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, config, kwargs={}, extra_dir=config.get('hyperopt_path')) @@ -81,8 +85,4 @@ class HyperOptLossResolver(IResolver): hyperoptloss.__class__.ticker_interval = 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 diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index b7d25ef2c..37cfd70e6 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -11,6 +11,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union from freqtrade.exceptions import OperationalException + logger = logging.getLogger(__name__) @@ -50,7 +51,8 @@ class IResolver: :param object_name: Class name of the object :param enum_failed: If True, will return None for modules which fail. 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 @@ -66,14 +68,16 @@ class IResolver: return iter([None]) valid_objects_gen = ( - obj for name, obj in inspect.getmembers(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) + (obj, inspect.getsource(module)) for + name, obj in inspect.getmembers( + 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 @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]]: """ 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) 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) @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]: """ Try to load object from path list. @@ -105,7 +112,8 @@ class IResolver: for _path in paths: try: (module, module_path) = cls._search_object(directory=_path, - object_name=object_name) + object_name=object_name, + add_source=add_source) if module: logger.info( f"Using resolved {cls.object_type.__name__.lower()[1:]} {object_name} " @@ -117,7 +125,7 @@ class IResolver: return None @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: """ Search and loads the specified object as configured in hte child class. @@ -132,10 +140,10 @@ class IResolver: user_subdir=cls.user_subdir, extra_dir=extra_dir) - pairlist = cls._load_object(paths=abs_paths, object_name=object_name, - kwargs=kwargs) - if pairlist: - return pairlist + found_object = cls._load_object(paths=abs_paths, object_name=object_name, + kwargs=kwargs) + if found_object: + return found_object raise OperationalException( f"Impossible to load {cls.object_type_str} '{object_name}'. This class does not exist " "or contains Python code errors." @@ -163,8 +171,8 @@ class IResolver: for obj in cls._get_valid_object(module_path, object_name=None, enum_failed=enum_failed): objects.append( - {'name': obj.__name__ if obj is not None else '', - 'class': obj, + {'name': obj[0].__name__ if obj is not None else '', + 'class': obj[0] if obj is not None else None, 'location': entry, }) return objects diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index 77db74084..4df5da37c 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -9,6 +9,7 @@ from pathlib import Path from freqtrade.pairlist.IPairList import IPairList from freqtrade.resolvers import IResolver + logger = logging.getLogger(__name__) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 121a04877..63a3f784e 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -11,12 +11,12 @@ from inspect import getfullargspec from pathlib import Path from typing import Any, Dict, Optional -from freqtrade.constants import (REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, - USERPATH_STRATEGIES) +from freqtrade.constants import REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, USERPATH_STRATEGIES from freqtrade.exceptions import OperationalException from freqtrade.resolvers import IResolver from freqtrade.strategy.interface import IStrategy + logger = logging.getLogger(__name__) @@ -174,7 +174,9 @@ class StrategyResolver(IResolver): strategy = StrategyResolver._load_object(paths=abs_paths, object_name=strategy_name, - kwargs={'config': config}) + add_source=True, + kwargs={'config': config}, + ) if strategy: strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) diff --git a/freqtrade/rpc/__init__.py b/freqtrade/rpc/__init__.py index 31c854f82..88978519b 100644 --- a/freqtrade/rpc/__init__.py +++ b/freqtrade/rpc/__init__.py @@ -1,2 +1,3 @@ -from .rpc import RPC, RPCMessageType, RPCException # noqa -from .rpc_manager import RPCManager # noqa +# flake8: noqa: F401 +from .rpc import RPC, RPCException, RPCMessageType +from .rpc_manager import RPCManager diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index db22ce453..be21179ad 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -1,40 +1,43 @@ import logging import threading +from copy import deepcopy from datetime import date, datetime from ipaddress import IPv4Address +from pathlib import Path from typing import Any, Callable, Dict from arrow import Arrow from flask import Flask, jsonify, request from flask.json import JSONEncoder from flask_cors import CORS -from flask_jwt_extended import (JWTManager, create_access_token, - create_refresh_token, get_jwt_identity, - jwt_refresh_token_required, +from flask_jwt_extended import (JWTManager, create_access_token, create_refresh_token, + get_jwt_identity, jwt_refresh_token_required, verify_jwt_in_request_optional) from werkzeug.security import safe_str_cmp from werkzeug.serving import make_server 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.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.rpc import RPC, RPCException + logger = logging.getLogger(__name__) BASE_URI = "/api/v1" -class ArrowJSONEncoder(JSONEncoder): +class FTJSONEncoder(JSONEncoder): def default(self, obj): try: if isinstance(obj, Arrow): return obj.for_json() - elif isinstance(obj, date): - return obj.strftime("%Y-%m-%d") elif isinstance(obj, datetime): return obj.strftime(DATETIME_PRINT_FORMAT) + elif isinstance(obj, date): + return obj.strftime("%Y-%m-%d") iterable = iter(obj) except TypeError: pass @@ -108,7 +111,7 @@ class ApiServer(RPC): 'jwt_secret_key', 'super-secret') self.jwt = JWTManager(self.app) - self.app.json_encoder = ArrowJSONEncoder + self.app.json_encoder = FTJSONEncoder self.app.teardown_appcontext(shutdown_session) @@ -160,16 +163,12 @@ class ApiServer(RPC): """ pass - def rest_dump(self, return_value): - """ Helper function to jsonify object for a webserver """ - return jsonify(return_value) - - def rest_error(self, error_msg): - return jsonify({"error": error_msg}), 502 + def rest_error(self, error_msg, error_code=502): + return jsonify({"error": error_msg}), error_code 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' 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', 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}/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}/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']) @@ -212,6 +212,20 @@ class ApiServer(RPC): view_func=self._trades, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/trades/', 'trades_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/', '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 self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist, methods=['GET', 'POST']) @@ -227,7 +241,7 @@ class ApiServer(RPC): """ Return "404 not found", 404. """ - return self.rest_dump({ + return jsonify({ 'status': 'error', 'reason': f"There's no API call for {request.base_url}.", 'code': 404 @@ -247,7 +261,7 @@ class ApiServer(RPC): 'access_token': create_access_token(identity=keystuff), 'refresh_token': create_refresh_token(identity=keystuff), } - return self.rest_dump(ret) + return jsonify(ret) return jsonify({"error": "Unauthorized"}), 401 @@ -262,7 +276,7 @@ class ApiServer(RPC): new_token = create_access_token(identity=current_user, fresh=False) ret = {'access_token': new_token} - return self.rest_dump(ret) + return jsonify(ret) @require_login @rpc_catch_errors @@ -272,7 +286,7 @@ class ApiServer(RPC): Starts TradeThread in bot if stopped. """ msg = self._rpc_start() - return self.rest_dump(msg) + return jsonify(msg) @require_login @rpc_catch_errors @@ -282,7 +296,7 @@ class ApiServer(RPC): Stops TradeThread in bot if running """ msg = self._rpc_stop() - return self.rest_dump(msg) + return jsonify(msg) @require_login @rpc_catch_errors @@ -292,14 +306,14 @@ class ApiServer(RPC): Sets max_open_trades to 0 and gracefully sells all open trades """ msg = self._rpc_stopbuy() - return self.rest_dump(msg) + return jsonify(msg) @rpc_catch_errors def _ping(self): """ - simple poing version + simple ping version """ - return self.rest_dump({"status": "pong"}) + return jsonify({"status": "pong"}) @require_login @rpc_catch_errors @@ -307,7 +321,7 @@ class ApiServer(RPC): """ Prints the bot's version """ - return self.rest_dump({"version": __version__}) + return jsonify({"version": __version__}) @require_login @rpc_catch_errors @@ -315,7 +329,7 @@ class ApiServer(RPC): """ Prints the bot's version """ - return self.rest_dump(self._rpc_show_config()) + return jsonify(self._rpc_show_config(self._config)) @require_login @rpc_catch_errors @@ -325,7 +339,7 @@ class ApiServer(RPC): Triggers a config file reload """ msg = self._rpc_reload_config() - return self.rest_dump(msg) + return jsonify(msg) @require_login @rpc_catch_errors @@ -335,7 +349,16 @@ class ApiServer(RPC): Returns the number of trades running """ 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 @rpc_catch_errors @@ -353,7 +376,7 @@ class ApiServer(RPC): self._config.get('fiat_display_currency', '') ) - return self.rest_dump(stats) + return jsonify(stats) @require_login @rpc_catch_errors @@ -365,7 +388,7 @@ class ApiServer(RPC): limit: Only get a certain number of records """ 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 @rpc_catch_errors @@ -376,7 +399,7 @@ class ApiServer(RPC): """ stats = self._rpc_edge() - return self.rest_dump(stats) + return jsonify(stats) @require_login @rpc_catch_errors @@ -392,7 +415,7 @@ class ApiServer(RPC): self._config.get('fiat_display_currency') ) - return self.rest_dump(stats) + return jsonify(stats) @require_login @rpc_catch_errors @@ -405,7 +428,7 @@ class ApiServer(RPC): """ stats = self._rpc_performance() - return self.rest_dump(stats) + return jsonify(stats) @require_login @rpc_catch_errors @@ -417,9 +440,9 @@ class ApiServer(RPC): """ try: results = self._rpc_trade_status() - return self.rest_dump(results) + return jsonify(results) except RPCException: - return self.rest_dump([]) + return jsonify([]) @require_login @rpc_catch_errors @@ -431,7 +454,7 @@ class ApiServer(RPC): """ results = self._rpc_balance(self._config['stake_currency'], self._config.get('fiat_display_currency', '')) - return self.rest_dump(results) + return jsonify(results) @require_login @rpc_catch_errors @@ -443,7 +466,7 @@ class ApiServer(RPC): """ limit = int(request.args.get('limit', 0)) results = self._rpc_trade_history(limit) - return self.rest_dump(results) + return jsonify(results) @require_login @rpc_catch_errors @@ -456,7 +479,7 @@ class ApiServer(RPC): tradeid: Numeric trade-id assigned to the trade. """ result = self._rpc_delete(tradeid) - return self.rest_dump(result) + return jsonify(result) @require_login @rpc_catch_errors @@ -465,7 +488,7 @@ class ApiServer(RPC): Handler for /whitelist. """ results = self._rpc_whitelist() - return self.rest_dump(results) + return jsonify(results) @require_login @rpc_catch_errors @@ -475,7 +498,7 @@ class ApiServer(RPC): """ add = request.json.get("blacklist", None) if request.method == 'POST' else None results = self._rpc_blacklist(add) - return self.rest_dump(results) + return jsonify(results) @require_login @rpc_catch_errors @@ -487,9 +510,9 @@ class ApiServer(RPC): price = request.json.get("price", None) trade = self._rpc_forcebuy(asset, price) if trade: - return self.rest_dump(trade.to_json()) + return jsonify(trade.to_json()) else: - return self.rest_dump({"status": f"Error buying pair {asset}."}) + return jsonify({"status": f"Error buying pair {asset}."}) @require_login @rpc_catch_errors @@ -499,4 +522,132 @@ class ApiServer(RPC): """ tradeid = request.json.get("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) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b32af1596..10aaf56fa 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -9,18 +9,22 @@ from math import isnan from typing import Any, Dict, List, Optional, Tuple, Union 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.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler 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.state import State from freqtrade.strategy.interface import SellType + logger = logging.getLogger(__name__) @@ -89,13 +93,12 @@ class RPC: def send_msg(self, msg: Dict[str, str]) -> None: """ 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. Explicitly does NOT return the full config to avoid leakage of sensitive information via rpc. """ - config = self._freqtrade.config val = { 'dry_run': config['dry_run'], 'stake_currency': config['stake_currency'], @@ -116,7 +119,7 @@ class RPC: 'forcebuy_enabled': config.get('forcebuy_enable', False), 'ask_strategy': config.get('ask_strategy', {}), 'bid_strategy': config.get('bid_strategy', {}), - 'state': str(self._freqtrade.state) + 'state': str(self._freqtrade.state) if self._freqtrade else '', } return val @@ -596,6 +599,17 @@ class RPC: '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: """ Returns the currently active whitelist""" res = {'method': self._freqtrade.pairlists.name_list, @@ -635,7 +649,7 @@ class RPC: buffer = bufferHandler.buffer[-limit:] else: 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.message + ('\n' + r.exc_text if r.exc_text else '')] for r in buffer] @@ -652,3 +666,82 @@ class RPC: if not self._freqtrade.edge: raise RPCException('Edge is not enabled.') 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 diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index e54749369..b97a5357b 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -6,6 +6,7 @@ from typing import Any, Dict, List from freqtrade.rpc import RPC, RPCMessageType + logger = logging.getLogger(__name__) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 87e52980a..3dcb7ab72 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,9 +5,9 @@ This module manage Telegram communication """ import json import logging -import arrow from typing import Any, Callable, Dict +import arrow from tabulate import tabulate from telegram import ParseMode, ReplyKeyboardMarkup, Update 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.fiat_convert import CryptoToFiatConverter + logger = logging.getLogger(__name__) logger.debug('Included module rpc.telegram ...') @@ -99,6 +100,7 @@ class Telegram(RPC): CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), + CommandHandler('locks', self._locks), CommandHandler(['reload_config', 'reload_conf'], self._reload_config), CommandHandler(['show_config', 'show_conf'], self._show_config), CommandHandler('stopbuy', self._stopbuy), @@ -607,6 +609,26 @@ class Telegram(RPC): except RPCException as 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 = "
{}
".format(message) + logger.debug(message) + self._send_msg(message, parse_mode=ParseMode.HTML) + except RPCException as e: + self._send_msg(str(e)) + @authorized_only def _whitelist(self, update: Update, context: CallbackContext) -> None: """ @@ -718,8 +740,8 @@ class Telegram(RPC): "*/delete :* `Instantly delete the given trade in the database`\n" "*/performance:* `Show performance of each finished trade grouped by pair`\n" "*/daily :* `Shows profit or loss per day, over the last n days`\n" - "*/count:* `Show number of trades running compared to allowed number of trades`" - "\n" + "*/count:* `Show number of active trades compared to allowed number of trades`\n" + "*/locks:* `Show currently locked pairs`\n" "*/balance:* `Show account balance per currency`\n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" "*/reload_config:* `Reload configuration file` \n" @@ -754,7 +776,7 @@ class Telegram(RPC): :param update: message update :return: None """ - val = self._rpc_show_config() + val = self._rpc_show_config(self._freqtrade.config) if val['trailing_stop']: sl_info = ( f"*Initial Stoploss:* `{val['stoploss']}`\n" diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index f089550c3..21413f165 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -2,9 +2,9 @@ This module manages webhook communication """ 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 diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index d1510489e..662156ae9 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -1,5 +1,5 @@ # flake8: noqa: F401 -from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_prev_date, - timeframe_to_seconds, timeframe_to_next_date, timeframe_to_msecs) +from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, + timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_helper import merge_informative_pair diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 92d9f6c48..1c6aa535d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -17,10 +17,11 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange.exchange import timeframe_to_next_date -from freqtrade.persistence import Trade +from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets + logger = logging.getLogger(__name__) @@ -122,6 +123,8 @@ class IStrategy(ABC): # and wallets - access to the current balance. dp: Optional[DataProvider] = None wallets: Optional[Wallets] = None + # container variable for strategy source code + __source__: str = '' # Definition of plot_config. See plotting documentation for more details. plot_config: Dict = {} @@ -130,7 +133,6 @@ class IStrategy(ABC): self.config = config # Dict to determine if analysis is necessary self._last_candle_seen_per_pair: Dict[str, datetime] = {} - self._pair_locked_until: Dict[str, datetime] = {} @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -275,7 +277,7 @@ class IStrategy(ABC): """ return self.__class__.__name__ - def lock_pair(self, pair: str, until: datetime) -> None: + def lock_pair(self, pair: str, until: datetime, reason: str = None) -> None: """ Locks pair until a given timestamp happens. Locked pairs are not analyzed, and are prevented from opening new trades. @@ -284,9 +286,9 @@ class IStrategy(ABC): :param pair: Pair to lock :param until: datetime in UTC until the pair should be blocked from opening new trades. Needs to be timezone aware `datetime.now(timezone.utc)` + :param reason: Optional string explaining why the pair was locked. """ - if pair not in self._pair_locked_until or self._pair_locked_until[pair] < until: - self._pair_locked_until[pair] = until + PairLocks.lock_pair(pair, until, reason) def unlock_pair(self, pair: str) -> None: """ @@ -295,8 +297,7 @@ class IStrategy(ABC): manually from within the strategy, to allow an easy way to unlock pairs. :param pair: Unlock pair to allow trading again """ - if pair in self._pair_locked_until: - del self._pair_locked_until[pair] + PairLocks.unlock_pair(pair, datetime.now(timezone.utc)) def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool: """ @@ -308,15 +309,13 @@ class IStrategy(ABC): :param candle_date: Date of the last candle. Optional, defaults to current date :returns: locking state of the pair in question. """ - if pair not in self._pair_locked_until: - return False + if not candle_date: - return self._pair_locked_until[pair] >= datetime.now(timezone.utc) + # Simple call ... + return PairLocks.is_pair_locked(pair, candle_date) else: - # Locking should happen until a new candle arrives lock_time = timeframe_to_next_date(self.timeframe, candle_date) - # lock_time = candle_date + timedelta(minutes=timeframe_to_minutes(self.timeframe)) - return self._pair_locked_until[pair] > lock_time + return PairLocks.is_pair_locked(pair, lock_time) def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 1a5b2d0f8..ea0e234ec 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -1,4 +1,5 @@ import pandas as pd + from freqtrade.exchange import timeframe_to_minutes diff --git a/freqtrade/strategy/strategy_wrapper.py b/freqtrade/strategy/strategy_wrapper.py index 8fc548074..121189b68 100644 --- a/freqtrade/strategy/strategy_wrapper.py +++ b/freqtrade/strategy/strategy_wrapper.py @@ -2,6 +2,7 @@ import logging from freqtrade.exceptions import StrategyError + logger = logging.getLogger(__name__) diff --git a/freqtrade/templates/sample_hyperopt.py b/freqtrade/templates/sample_hyperopt.py index 0b6d030db..10743e911 100644 --- a/freqtrade/templates/sample_hyperopt.py +++ b/freqtrade/templates/sample_hyperopt.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# isort: skip_file # --- Do not remove these libs --- from functools import reduce diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index 7f05c4430..52e397466 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -1,5 +1,5 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement - +# isort: skip_file # --- Do not remove these libs --- from functools import reduce from typing import Any, Callable, Dict, List diff --git a/freqtrade/templates/sample_hyperopt_loss.py b/freqtrade/templates/sample_hyperopt_loss.py index 4173d97f5..59e6d814a 100644 --- a/freqtrade/templates/sample_hyperopt_loss.py +++ b/freqtrade/templates/sample_hyperopt_loss.py @@ -1,10 +1,11 @@ -from math import exp from datetime import datetime +from math import exp from pandas import DataFrame from freqtrade.optimize.hyperopt import IHyperOptLoss + # Define some constants: # set TARGET_TRADES to suit your number concurrent trades so its realistic diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index e269848d2..103f68a43 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -1,5 +1,5 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement - +# isort: skip_file # --- Do not remove these libs --- import numpy as np # noqa import pandas as pd # noqa diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py index e5a404862..4c0fb5b5c 100644 --- a/freqtrade/vendor/qtpylib/indicators.py +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -19,14 +19,15 @@ # limitations under the License. # -import warnings import sys +import warnings from datetime import datetime, timedelta import numpy as np import pandas as pd from pandas.core.base import PandasObject + # ============================================= # check min, python version if sys.version_info < (3, 4): diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index ac08f337c..21a9466e1 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -10,6 +10,7 @@ import arrow from freqtrade.exchange import Exchange from freqtrade.persistence import Trade + logger = logging.getLogger(__name__) diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 2fc206bd5..ec9331eef 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -15,6 +15,7 @@ from freqtrade.exceptions import OperationalException, TemporaryError from freqtrade.freqtradebot import FreqtradeBot from freqtrade.state import State + logger = logging.getLogger(__name__) diff --git a/mkdocs.yml b/mkdocs.yml index 26494ae45..8d1ce1cfe 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -55,16 +55,16 @@ markdown_extensions: permalink: true - pymdownx.arithmatex: generic: true - - pymdownx.caret - - pymdownx.critic - pymdownx.details - pymdownx.inlinehilite - pymdownx.magiclink - - pymdownx.mark + - pymdownx.pathconverter - pymdownx.smartsymbols + - pymdownx.snippets: + base_path: docs + check_paths: true - pymdownx.tabbed - pymdownx.superfences - pymdownx.tasklist: custom_checkbox: true - - pymdownx.tilde - mdx_truly_sane_lists diff --git a/requirements-dev.txt b/requirements-dev.txt index ffe2763a6..916bb2ec2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,15 +4,16 @@ -r requirements-hyperopt.txt coveralls==2.1.2 -flake8==3.8.3 +flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 -mypy==0.782 -pytest==6.0.2 +mypy==0.790 +pytest==6.1.1 pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-mock==3.3.1 pytest-random-order==1.0.4 +isort==5.6.4 # Convert jupyter notebooks to markdown documents -nbconvert==6.0.4 +nbconvert==6.0.7 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index b47331aa3..5b68c1ea1 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,9 +2,9 @@ -r requirements.txt # Required for hyperopt -scipy==1.5.2 +scipy==1.5.3 scikit-learn==0.23.2 scikit-optimize==0.8.1 filelock==3.0.12 -joblib==0.16.0 +joblib==0.17.0 progressbar2==3.53.1 diff --git a/requirements-plot.txt b/requirements-plot.txt index a91b3bd38..bd40bc0b5 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==4.10.0 +plotly==4.12.0 diff --git a/requirements.txt b/requirements.txt index 44d2f29a2..7d2017beb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,19 @@ numpy==1.19.2 -pandas==1.1.2 +pandas==1.1.3 -ccxt==1.34.40 -SQLAlchemy==1.3.19 -python-telegram-bot==12.8 -arrow==0.16.0 +ccxt==1.36.85 +aiohttp==3.7.1 +SQLAlchemy==1.3.20 +python-telegram-bot==13.0 +arrow==0.17.0 cachetools==4.1.1 requests==2.24.0 -urllib3==1.25.10 +urllib3==1.25.11 wrapt==1.12.1 jsonschema==3.2.0 -TA-Lib==0.4.18 +TA-Lib==0.4.19 tabulate==0.8.7 -pycoingecko==1.3.0 +pycoingecko==1.4.0 jinja2==2.11.2 tables==3.6.1 blosc==1.9.2 @@ -21,7 +22,7 @@ blosc==1.9.2 py_find_1st==1.1.4 # Load ticker files 30% faster -python-rapidjson==0.9.1 +python-rapidjson==0.9.3 # Notify systemd sdnotify==0.3.2 @@ -32,7 +33,7 @@ flask-jwt-extended==3.24.1 flask-cors==3.0.9 # Support for colorized terminal output -colorama==0.4.3 +colorama==0.4.4 # Building config files interactively -questionary==1.5.2 -prompt-toolkit==3.0.7 +questionary==1.7.0 +prompt-toolkit==3.0.8 diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 95685fd64..268e81397 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -10,8 +10,8 @@ so it can be used as a standalone script. import argparse import inspect import json -import re import logging +import re import sys from pathlib import Path from urllib.parse import urlencode, urlparse, urlunparse @@ -20,6 +20,7 @@ import rapidjson import requests from requests.exceptions import ConnectionError + logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', @@ -110,6 +111,13 @@ class FtRestClient(): """ return self._get("count") + def locks(self): + """Return current locks + + :return: json object + """ + return self._get("locks") + def daily(self, days=None): """Return the amount of open trades. @@ -223,6 +231,70 @@ class FtRestClient(): return self._post("forcesell", data={"tradeid": tradeid}) + def strategies(self): + """Lists available strategies + + :return: json object + """ + return self._get("strategies") + + def strategy(self, strategy): + """Get strategy details + + :param strategy: Strategy class name + :return: json object + """ + return self._get(f"strategy/{strategy}") + + def plot_config(self): + """Return plot configuration if the strategy defines one. + + :return: json object + """ + return self._get("plot_config") + + def available_pairs(self, timeframe=None, stake_currency=None): + """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 + :return: json object + """ + return self._get("available_pairs", params={ + "stake_currency": stake_currency if timeframe else '', + "timeframe": timeframe if timeframe else '', + }) + + def pair_candles(self, pair, timeframe, limit=None): + """Return live dataframe for . + + :param pair: Pair to get data for + :param timeframe: Only pairs with this timeframe available. + :param limit: Limit result to the last n candles. + :return: json object + """ + return self._get("available_pairs", params={ + "pair": pair, + "timeframe": timeframe, + "limit": limit, + }) + + def pair_history(self, pair, timeframe, strategy, timerange=None): + """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) + :return: json object + """ + return self._get("pair_history", params={ + "pair": pair, + "timeframe": timeframe, + "strategy": strategy, + "timerange": timerange if timerange else '', + }) + def add_arguments(): parser = argparse.ArgumentParser() diff --git a/setup.cfg b/setup.cfg index 34f25482b..be2cd450c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,6 +8,11 @@ exclude = .eggs, user_data, +[isort] +line_length=100 +multi_line_output=0 +lines_after_imports=2 + [mypy] ignore_missing_imports = True diff --git a/setup.py b/setup.py index 88d754668..9b57e8d2c 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,15 @@ from sys import version_info + from setuptools import setup + if version_info.major == 3 and version_info.minor < 6 or \ version_info.major < 3: print('Your Python interpreter must be 3.6 or greater!') exit(1) from pathlib import Path # noqa: E402 + from freqtrade import __version__ # noqa: E402 @@ -108,6 +111,7 @@ setup(name='freqtrade', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Operating System :: MacOS', 'Operating System :: Unix', 'Topic :: Office/Business :: Financial :: Investment', diff --git a/tests/commands/test_build_config.py b/tests/commands/test_build_config.py index 69b277e3b..291720f4b 100644 --- a/tests/commands/test_build_config.py +++ b/tests/commands/test_build_config.py @@ -4,10 +4,8 @@ from unittest.mock import MagicMock import pytest import rapidjson -from freqtrade.commands.build_config_commands import (ask_user_config, - ask_user_overwrite, - start_new_config, - validate_is_float, +from freqtrade.commands.build_config_commands import (ask_user_config, ask_user_overwrite, + start_new_config, validate_is_float, validate_is_int) from freqtrade.exceptions import OperationalException from tests.conftest import get_args, log_has_re diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 192e125f8..a392b74cf 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -5,19 +5,16 @@ from unittest.mock import MagicMock, PropertyMock import arrow import pytest -from freqtrade.commands import (start_convert_data, start_create_userdir, - start_download_data, start_hyperopt_list, - start_hyperopt_show, start_list_data, - start_list_exchanges, start_list_hyperopts, - start_list_markets, start_list_strategies, - start_list_timeframes, start_new_hyperopt, - start_new_strategy, start_show_trades, - start_test_pairlist, start_trading) +from freqtrade.commands import (start_convert_data, start_create_userdir, start_download_data, + start_hyperopt_list, start_hyperopt_show, start_list_data, + start_list_exchanges, start_list_hyperopts, start_list_markets, + start_list_strategies, start_list_timeframes, start_new_hyperopt, + start_new_strategy, start_show_trades, start_test_pairlist, + start_trading) from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode -from tests.conftest import (create_mock_trades, get_args, log_has, log_has_re, - patch_exchange, +from tests.conftest import (create_mock_trades, get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) from tests.conftest_trades import MOCK_TRADE_COUNT @@ -438,6 +435,16 @@ def test_list_markets(mocker, markets, capsys): assert re.search(r"^BLK/BTC$", captured.out, re.MULTILINE) assert re.search(r"^LTC/USD$", captured.out, re.MULTILINE) + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(side_effect=ValueError)) + # Test --one-column + args = [ + "list-markets", + '--config', 'config.json.example', + "--one-column" + ] + with pytest.raises(OperationalException, match=r"Cannot get markets.*"): + start_list_markets(get_args(args), False) + def test_create_datadir_failed(caplog): @@ -479,6 +486,12 @@ def test_start_new_strategy(mocker, caplog): assert "CoolNewStrategy" in wt_mock.call_args_list[0][0][0] assert log_has_re("Writing strategy to .*", caplog) + mocker.patch('freqtrade.commands.deploy_commands.setup_utils_configuration') + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + with pytest.raises(OperationalException, + match=r".* already exists. Please choose another Strategy Name\."): + start_new_strategy(get_args(args)) + def test_start_new_strategy_DefaultStrat(mocker, caplog): args = [ @@ -515,6 +528,12 @@ def test_start_new_hyperopt(mocker, caplog): assert "CoolNewhyperopt" in wt_mock.call_args_list[0][0][0] assert log_has_re("Writing hyperopt to .*", caplog) + mocker.patch('freqtrade.commands.deploy_commands.setup_utils_configuration') + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + with pytest.raises(OperationalException, + match=r".* already exists. Please choose another Hyperopt Name\."): + start_new_hyperopt(get_args(args)) + def test_start_new_hyperopt_DefaultHyperopt(mocker, caplog): args = [ @@ -698,6 +717,7 @@ def test_start_list_strategies(mocker, caplog, capsys): "list-strategies", "--strategy-path", str(Path(__file__).parent.parent / "strategy" / "strats"), + '--no-color', ] pargs = get_args(args) # pargs['config'] = None @@ -772,6 +792,25 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): assert re.match(r"Pairs for .*", captured.out) assert re.match("['ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC', 'XRP/BTC']", captured.out) + args = [ + 'test-pairlist', + '-c', 'config.json.example', + '--one-column', + ] + start_test_pairlist(get_args(args)) + captured = capsys.readouterr() + assert re.match(r"ETH/BTC\nTKN/BTC\nBLK/BTC\nLTC/BTC\nXRP/BTC\n", captured.out) + + args = [ + 'test-pairlist', + '-c', 'config.json.example', + '--print-json', + ] + start_test_pairlist(get_args(args)) + captured = capsys.readouterr() + assert re.match(r'Pairs for BTC: \n\["ETH/BTC","TKN/BTC","BLK/BTC","LTC/BTC","XRP/BTC"\]\n', + captured.out) + def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): mocker.patch( @@ -1152,7 +1191,7 @@ def test_start_list_data(testdatadir, capsys): @pytest.mark.usefixtures("init_persistence") def test_show_trades(mocker, fee, capsys, caplog): - mocker.patch("freqtrade.persistence.init") + mocker.patch("freqtrade.persistence.init_db") create_mock_trades(fee) args = [ "show-trades", diff --git a/tests/conftest.py b/tests/conftest.py index fe55c8784..520b53b31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,17 +13,18 @@ import numpy as np import pytest from telegram import Chat, Message, Update -from freqtrade import constants, persistence +from freqtrade import constants from freqtrade.commands import Arguments from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import Trade +from freqtrade.persistence import Trade, init_db from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker -from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, - mock_trade_4, mock_trade_5, mock_trade_6) +from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, + mock_trade_5, mock_trade_6) + logging.getLogger('').setLevel(logging.INFO) @@ -130,7 +131,7 @@ def patch_freqtradebot(mocker, config) -> None: :return: None """ mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - persistence.init(config['db_url']) + init_db(config['db_url']) patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) @@ -145,6 +146,7 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: :return: FreqtradeBot """ patch_freqtradebot(mocker, config) + config['datadir'] = Path(config['datadir']) return FreqtradeBot(config) @@ -217,7 +219,7 @@ def patch_coingekko(mocker) -> None: @pytest.fixture(scope='function') def init_persistence(default_conf): - persistence.init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) @pytest.fixture(scope="function") @@ -295,7 +297,7 @@ def default_conf(testdatadir): @pytest.fixture def update(): _update = Update(0) - _update.message = Message(0, 0, datetime.utcnow(), Chat(0, 0)) + _update.message = Message(0, datetime.utcnow(), Chat(0, 0)) return _update diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 564dae0b1..1592fac10 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -7,15 +7,11 @@ from pandas import DataFrame, DateOffset, Timestamp, to_datetime from freqtrade.configuration import TimeRange from freqtrade.constants import LAST_BT_RESULT_FN -from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, - analyze_trade_parallelism, - calculate_market_change, - calculate_max_drawdown, - combine_dataframes_with_mean, - create_cum_profit, - extract_trades_of_period, - get_latest_backtest_filename, - load_backtest_data, load_trades, +from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelism, + calculate_market_change, calculate_max_drawdown, + combine_dataframes_with_mean, create_cum_profit, + extract_trades_of_period, get_latest_backtest_filename, + get_latest_hyperopt_file, load_backtest_data, load_trades, load_trades_from_db) from freqtrade.data.history import load_data, load_pair_history from freqtrade.optimize.backtesting import BacktestResult @@ -43,6 +39,17 @@ def test_get_latest_backtest_filename(testdatadir, mocker): get_latest_backtest_filename(testdatadir) +def test_get_latest_hyperopt_file(testdatadir, mocker): + res = get_latest_hyperopt_file(testdatadir / 'does_not_exist', 'testfile.pickle') + assert res == testdatadir / 'does_not_exist/testfile.pickle' + + res = get_latest_hyperopt_file(testdatadir.parent) + assert res == testdatadir.parent / "hyperopt_results.pickle" + + res = get_latest_hyperopt_file(str(testdatadir.parent)) + assert res == testdatadir.parent / "hyperopt_results.pickle" + + def test_load_backtest_data_old_format(testdatadir): filename = testdatadir / "backtest-result_test.json" @@ -107,7 +114,7 @@ def test_load_trades_from_db(default_conf, fee, mocker): create_mock_trades(fee) # remove init so it does not init again - init_mock = mocker.patch('freqtrade.persistence.init', MagicMock()) + init_mock = mocker.patch('freqtrade.data.btanalysis.init_db', MagicMock()) trades = load_trades_from_db(db_url=default_conf['db_url']) assert init_mock.call_count == 1 diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 4a580366f..fdba7900f 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -2,13 +2,11 @@ import logging from freqtrade.configuration.timerange import TimeRange -from freqtrade.data.converter import (convert_ohlcv_format, - convert_trades_format, - ohlcv_fill_up_missing_data, - ohlcv_to_dataframe, trades_dict_to_list, - trades_remove_duplicates, trim_dataframe) -from freqtrade.data.history import (get_timerange, load_data, - load_pair_history, validate_backtest_data) +from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_format, + ohlcv_fill_up_missing_data, ohlcv_to_dataframe, + trades_dict_to_list, trades_remove_duplicates, trim_dataframe) +from freqtrade.data.history import (get_timerange, load_data, load_pair_history, + validate_backtest_data) from tests.conftest import log_has from tests.data.test_history import _backup_file, _clean_test_file diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index c2ecf4b80..a64dce908 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -132,7 +132,7 @@ def test_orderbook(mocker, default_conf, order_book_l2): res = dp.orderbook('ETH/BTC', 5) assert order_book_l2.call_count == 1 assert order_book_l2.call_args_list[0][0][0] == 'ETH/BTC' - assert order_book_l2.call_args_list[0][0][1] == 5 + assert order_book_l2.call_args_list[0][0][1] >= 5 assert type(res) is dict assert 'bids' in res diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 787f62a75..c8324cf0b 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -15,20 +15,19 @@ from freqtrade.configuration import TimeRange from freqtrade.constants import AVAILABLE_DATAHANDLERS from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.data.history.hdf5datahandler import HDF5DataHandler -from freqtrade.data.history.history_utils import ( - _download_pair_history, _download_trades_history, - _load_cached_data_for_updating, convert_trades_to_ohlcv, get_timerange, - load_data, load_pair_history, refresh_backtest_ohlcv_data, - refresh_backtest_trades_data, refresh_data, validate_backtest_data) -from freqtrade.data.history.idatahandler import (IDataHandler, get_datahandler, - get_datahandlerclass) -from freqtrade.data.history.jsondatahandler import (JsonDataHandler, - JsonGzDataHandler) +from freqtrade.data.history.history_utils import (_download_pair_history, _download_trades_history, + _load_cached_data_for_updating, + convert_trades_to_ohlcv, get_timerange, load_data, + load_pair_history, refresh_backtest_ohlcv_data, + refresh_backtest_trades_data, refresh_data, + validate_backtest_data) +from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler, get_datahandlerclass +from freqtrade.data.history.jsondatahandler import JsonDataHandler, JsonGzDataHandler from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json from freqtrade.resolvers import StrategyResolver -from tests.conftest import (get_patched_exchange, log_has, log_has_re, - patch_exchange) +from tests.conftest import get_patched_exchange, log_has, log_has_re, patch_exchange + # Change this if modifying UNITTEST/BTC testdatafile _BTC_UNITTEST_LENGTH = 13681 diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index d35f7fcf6..a4bfa1085 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -10,14 +10,15 @@ import numpy as np import pytest from pandas import DataFrame, to_datetime -from freqtrade.exceptions import OperationalException from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo +from freqtrade.exceptions import OperationalException from freqtrade.strategy.interface import SellType from tests.conftest import get_patched_freqtradebot, log_has from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, _get_frame_time_from_offset) + # Cases to be tested: # 1) Open trade should be removed from the end # 2) Two complete trades within dataframe (with sell hit for all) @@ -498,3 +499,61 @@ def test_process_expectancy_remove_pumps(mocker, edge_conf, fee,): assert final['TEST/BTC'].stoploss == -0.9 assert final['TEST/BTC'].nb_trades == len(trades_df) - 1 assert round(final['TEST/BTC'].winrate, 10) == 0.0 + + +def test_process_expectancy_only_wins(mocker, edge_conf, fee,): + edge_conf['edge']['min_trade_number'] = 2 + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + + freqtrade.exchange.get_fee = fee + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) + + trades = [ + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_date': np.datetime64('2018-10-03T00:05:00.000000000'), + 'close_date': np.datetime64('2018-10-03T00:10:00.000000000'), + 'open_index': 1, + 'close_index': 1, + 'trade_duration': '', + 'open_rate': 15, + 'close_rate': 17, + 'exit_type': 'sell_signal'}, + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_date': np.datetime64('2018-10-03T00:20:00.000000000'), + 'close_date': np.datetime64('2018-10-03T00:25:00.000000000'), + 'open_index': 4, + 'close_index': 4, + 'trade_duration': '', + 'open_rate': 10, + 'close_rate': 20, + 'exit_type': 'sell_signal'}, + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_date': np.datetime64('2018-10-03T00:30:00.000000000'), + 'close_date': np.datetime64('2018-10-03T00:40:00.000000000'), + 'open_index': 6, + 'close_index': 7, + 'trade_duration': '', + 'open_rate': 26, + 'close_rate': 134, + 'exit_type': 'sell_signal'} + ] + + trades_df = DataFrame(trades) + trades_df = edge._fill_calculable_fields(trades_df) + final = edge._process_expectancy(trades_df) + + assert 'TEST/BTC' in final + assert final['TEST/BTC'].stoploss == -0.9 + assert final['TEST/BTC'].nb_trades == len(trades_df) + assert round(final['TEST/BTC'].winrate, 10) == 1.0 + assert round(final['TEST/BTC'].risk_reward_ratio, 10) == float('inf') + assert round(final['TEST/BTC'].expectancy, 10) == float('inf') diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 72da708b4..f2b508761 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -4,8 +4,7 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException) +from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException from tests.conftest import get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e0b97d157..a01700e5d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1,6 +1,6 @@ import copy import logging -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from random import randint from unittest.mock import MagicMock, Mock, PropertyMock, patch @@ -9,21 +9,18 @@ import ccxt import pytest from pandas import DataFrame -from freqtrade.exceptions import (DDosProtection, DependencyException, - InvalidOrderException, OperationalException, - TemporaryError) -from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.common import (API_RETRY_COUNT, API_FETCH_ORDER_RETRY_COUNT, +from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, + OperationalException, TemporaryError) +from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken +from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT, calculate_backoff) -from freqtrade.exchange.exchange import (market_is_active, - timeframe_to_minutes, - timeframe_to_msecs, - timeframe_to_next_date, - timeframe_to_prev_date, +from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_msecs, + timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.resolvers.exchange_resolver import ExchangeResolver from tests.conftest import get_patched_exchange, log_has, log_has_re + # Make sure to always keep one exchange here which is NOT subclassed!! EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx'] @@ -151,11 +148,19 @@ def test_exchange_resolver(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - exchange = ExchangeResolver.load_exchange('Bittrex', default_conf) + + exchange = ExchangeResolver.load_exchange('huobi', default_conf) assert isinstance(exchange, Exchange) assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) caplog.clear() + exchange = ExchangeResolver.load_exchange('Bittrex', default_conf) + assert isinstance(exchange, Exchange) + assert isinstance(exchange, Bittrex) + assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", + caplog) + caplog.clear() + exchange = ExchangeResolver.load_exchange('kraken', default_conf) assert isinstance(exchange, Exchange) assert isinstance(exchange, Kraken) @@ -1290,6 +1295,15 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): # Returns twice the above OHLCV data assert len(ret) == 2 + caplog.clear() + + async def mock_get_candle_hist_error(pair, *args, **kwargs): + raise TimeoutError() + + exchange._async_get_candle_history = MagicMock(side_effect=mock_get_candle_hist_error) + ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000)) + assert log_has_re(r"Async code raised an exception: .*", caplog) + def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: ohlcv = [ @@ -1441,6 +1455,27 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog): assert log_has("Async code raised an exception: TypeError", caplog) +def test_get_next_limit_in_list(): + limit_range = [5, 10, 20, 50, 100, 500, 1000] + assert Exchange.get_next_limit_in_list(1, limit_range) == 5 + assert Exchange.get_next_limit_in_list(5, limit_range) == 5 + assert Exchange.get_next_limit_in_list(6, limit_range) == 10 + assert Exchange.get_next_limit_in_list(9, limit_range) == 10 + assert Exchange.get_next_limit_in_list(10, limit_range) == 10 + assert Exchange.get_next_limit_in_list(11, limit_range) == 20 + assert Exchange.get_next_limit_in_list(19, limit_range) == 20 + assert Exchange.get_next_limit_in_list(21, limit_range) == 50 + assert Exchange.get_next_limit_in_list(51, limit_range) == 100 + assert Exchange.get_next_limit_in_list(1000, limit_range) == 1000 + # Going over the limit ... + assert Exchange.get_next_limit_in_list(1001, limit_range) == 1000 + assert Exchange.get_next_limit_in_list(2000, limit_range) == 1000 + + assert Exchange.get_next_limit_in_list(21, None) == 21 + assert Exchange.get_next_limit_in_list(100, None) == 100 + assert Exchange.get_next_limit_in_list(1000, None) == 1000 + + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_fetch_l2_order_book(default_conf, mocker, order_book_l2, exchange_name): default_conf['exchange']['name'] = exchange_name @@ -1453,6 +1488,19 @@ def test_fetch_l2_order_book(default_conf, mocker, order_book_l2, exchange_name) assert 'asks' in order_book assert len(order_book['bids']) == 10 assert len(order_book['asks']) == 10 + assert api_mock.fetch_l2_order_book.call_args_list[0][0][0] == 'ETH/BTC' + + for val in [1, 5, 10, 12, 20, 50, 100]: + api_mock.fetch_l2_order_book.reset_mock() + + order_book = exchange.fetch_l2_order_book(pair='ETH/BTC', limit=val) + assert api_mock.fetch_l2_order_book.call_args_list[0][0][0] == 'ETH/BTC' + # Not all exchanges support all limits for orderbook + if not exchange._ft_has['l2_limit_range'] or val in exchange._ft_has['l2_limit_range']: + assert api_mock.fetch_l2_order_book.call_args_list[0][0][1] == val + else: + next_limit = exchange.get_next_limit_in_list(val, exchange._ft_has['l2_limit_range']) + assert api_mock.fetch_l2_order_book.call_args_list[0][0][1] == next_limit @pytest.mark.parametrize("exchange_name", EXCHANGES) @@ -2252,6 +2300,9 @@ def test_timeframe_to_next_date(): date = datetime.now(tz=timezone.utc) assert timeframe_to_next_date("5m") > date + date = datetime(2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc) + assert timeframe_to_next_date("5m", date) == date + timedelta(minutes=5) + @pytest.mark.parametrize("market_symbol,base,quote,exchange,add_dict,expected_result", [ ("BTC/USDT", 'BTC', 'USDT', "binance", {}, True), diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 16789af2c..17cfb26fa 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -10,6 +10,7 @@ from tests.conftest import get_patched_exchange from .test_exchange import ccxt_exceptionhandlers + STOPLOSS_ORDERTYPE = 'stop' diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 8f774a7ec..31b79a202 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -8,6 +8,7 @@ from freqtrade.exceptions import DependencyException, InvalidOrderException from tests.conftest import get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers + STOPLOSS_ORDERTYPE = 'stop-loss' diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index 8bc66f02c..306850ff6 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -6,6 +6,7 @@ from pandas import DataFrame from freqtrade.exchange import timeframe_to_minutes from freqtrade.strategy.interface import SellType + tests_start_time = arrow.get(2018, 10, 3) tests_timeframe = '1h' diff --git a/tests/optimize/conftest.py b/tests/optimize/conftest.py new file mode 100644 index 000000000..f06b0ecd3 --- /dev/null +++ b/tests/optimize/conftest.py @@ -0,0 +1,51 @@ +from copy import deepcopy +from datetime import datetime +from pathlib import Path + +import pandas as pd +import pytest + +from freqtrade.optimize.hyperopt import Hyperopt +from freqtrade.strategy.interface import SellType +from tests.conftest import patch_exchange + + +@pytest.fixture(scope='function') +def hyperopt_conf(default_conf): + hyperconf = deepcopy(default_conf) + hyperconf.update({ + 'hyperopt': 'DefaultHyperOpt', + 'hyperopt_loss': 'ShortTradeDurHyperOptLoss', + 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), + 'epochs': 1, + 'timerange': None, + 'spaces': ['default'], + 'hyperopt_jobs': 1, + }) + return hyperconf + + +@pytest.fixture(scope='function') +def hyperopt(hyperopt_conf, mocker): + + patch_exchange(mocker) + return Hyperopt(hyperopt_conf) + + +@pytest.fixture(scope='function') +def hyperopt_results(): + return pd.DataFrame( + { + 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'profit_percent': [-0.1, 0.2, 0.3], + 'profit_abs': [-0.2, 0.4, 0.6], + 'trade_duration': [10, 30, 10], + 'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI], + 'close_date': + [ + datetime(2019, 1, 1, 9, 26, 3, 478039), + datetime(2019, 2, 1, 9, 26, 3, 478039), + datetime(2019, 3, 1, 9, 26, 3, 478039) + ] + } + ) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index f6ac95aeb..a5de64fe4 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -11,6 +11,7 @@ from tests.conftest import patch_exchange from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, _get_frame_time_from_offset, tests_timeframe) + # Test 0: Sell with signal sell in candle 3 # Test with Stop-loss at 1% tc0 = BTContainer(data=[ diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 78a7130f9..45cbea68e 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -10,8 +10,7 @@ import pytest from arrow import Arrow from freqtrade import constants -from freqtrade.commands.optimize_commands import (setup_optimize_configuration, - start_backtesting) +from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi @@ -26,6 +25,7 @@ from freqtrade.strategy.interface import SellType from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) + ORDER_TYPES = [ { 'buy': 'limit', diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index d58b91209..82be894d3 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1,11 +1,11 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 import locale import logging +import re from datetime import datetime from pathlib import Path -from copy import deepcopy from typing import Dict, List -from unittest.mock import MagicMock, PropertyMock +from unittest.mock import MagicMock import pandas as pd import pytest @@ -13,62 +13,18 @@ from arrow import Arrow from filelock import Timeout from freqtrade import constants -from freqtrade.commands.optimize_commands import (setup_optimize_configuration, - start_hyperopt) +from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.optimize.default_hyperopt_loss import DefaultHyperOptLoss from freqtrade.optimize.hyperopt import Hyperopt -from freqtrade.resolvers.hyperopt_resolver import (HyperOptLossResolver, - HyperOptResolver) +from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode -from freqtrade.strategy.interface import SellType from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) from .hyperopts.default_hyperopt import DefaultHyperOpt -@pytest.fixture(scope='function') -def hyperopt_conf(default_conf): - hyperconf = deepcopy(default_conf) - hyperconf.update({ - 'hyperopt': 'DefaultHyperOpt', - 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), - 'epochs': 1, - 'timerange': None, - 'spaces': ['default'], - 'hyperopt_jobs': 1, - }) - return hyperconf - - -@pytest.fixture(scope='function') -def hyperopt(hyperopt_conf, mocker): - - patch_exchange(mocker) - return Hyperopt(hyperopt_conf) - - -@pytest.fixture(scope='function') -def hyperopt_results(): - return pd.DataFrame( - { - 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], - 'profit_percent': [-0.1, 0.2, 0.3], - 'profit_abs': [-0.2, 0.4, 0.6], - 'trade_duration': [10, 30, 10], - 'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI], - 'close_date': - [ - datetime(2019, 1, 1, 9, 26, 3, 478039), - datetime(2019, 2, 1, 9, 26, 3, 478039), - datetime(2019, 3, 1, 9, 26, 3, 478039) - ] - } - ) - - # Functions for recurrent object patching def create_results(mocker, hyperopt, testdatadir) -> List[Dict]: """ @@ -81,13 +37,14 @@ def create_results(mocker, hyperopt, testdatadir) -> List[Dict]: mocker.patch.object(Path, "is_file", MagicMock(return_value=False)) stat_mock = MagicMock() - stat_mock.st_size = PropertyMock(return_value=1) - mocker.patch.object(Path, "stat", MagicMock(return_value=False)) + stat_mock.st_size = 1 + mocker.patch.object(Path, "stat", MagicMock(return_value=stat_mock)) mocker.patch.object(Path, "unlink", MagicMock(return_value=True)) mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') - return [{'loss': 1, 'result': 'foo', 'params': {}}] + return [{'loss': 1, 'result': 'foo', 'params': {}, 'is_best': True}] def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None: @@ -229,24 +186,6 @@ def test_hyperoptresolver_noname(default_conf): HyperOptResolver.load_hyperopt(default_conf) -def test_hyperoptlossresolver(mocker, default_conf) -> None: - - hl = DefaultHyperOptLoss - mocker.patch( - 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object', - MagicMock(return_value=hl) - ) - x = HyperOptLossResolver.load_hyperoptloss(default_conf) - assert hasattr(x, "hyperopt_loss_function") - - -def test_hyperoptlossresolver_wrongname(default_conf) -> None: - default_conf.update({'hyperopt_loss': "NonExistingLossClass"}) - - with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'): - HyperOptLossResolver.load_hyperoptloss(default_conf) - - def test_start_not_installed(mocker, default_conf, import_fails) -> None: start_mock = MagicMock() patched_configuration_load_config_file(mocker, default_conf) @@ -260,7 +199,8 @@ def test_start_not_installed(mocker, default_conf, import_fails) -> None: '--hyperopt', 'DefaultHyperOpt', '--hyperopt-path', str(Path(__file__).parent / "hyperopts"), - '--epochs', '5' + '--epochs', '5', + '--hyperopt-loss', 'SharpeHyperOptLossDaily', ] pargs = get_args(args) @@ -278,6 +218,7 @@ def test_start(mocker, hyperopt_conf, caplog) -> None: 'hyperopt', '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', + '--hyperopt-loss', 'SharpeHyperOptLossDaily', '--epochs', '5' ] pargs = get_args(args) @@ -301,6 +242,7 @@ def test_start_no_data(mocker, hyperopt_conf) -> None: 'hyperopt', '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', + '--hyperopt-loss', 'SharpeHyperOptLossDaily', '--epochs', '5' ] pargs = get_args(args) @@ -318,6 +260,7 @@ def test_start_filelock(mocker, hyperopt_conf, caplog) -> None: 'hyperopt', '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', + '--hyperopt-loss', 'SharpeHyperOptLossDaily', '--epochs', '5' ] pargs = get_args(args) @@ -325,137 +268,6 @@ def test_start_filelock(mocker, hyperopt_conf, caplog) -> None: assert log_has("Another running instance of freqtrade Hyperopt detected.", caplog) -def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_results) -> None: - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, 600, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over > correct - assert under > correct - - -def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results) -> None: - resultsb = hyperopt_results.copy() - resultsb.loc[1, 'trade_duration'] = 20 - - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - longer = hl.hyperopt_loss_function(hyperopt_results, 100, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - shorter = hl.hyperopt_loss_function(resultsb, 100, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert shorter < longer - - -def test_loss_calculation_has_limited_profit(default_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, 600, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, 600, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, 600, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - -def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'}) - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - -def test_sharpe_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - -def test_sortino_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - default_conf.update({'hyperopt_loss': 'SortinoHyperOptLoss'}) - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - -def test_sortino_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - default_conf.update({'hyperopt_loss': 'SortinoHyperOptLossDaily'}) - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - -def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'}) - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - def test_log_results_if_loss_improves(hyperopt, capsys) -> None: hyperopt.current_best_loss = 2 hyperopt.total_epochs = 2 @@ -497,6 +309,7 @@ def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None: def test_save_results_saves_epochs(mocker, hyperopt, testdatadir, caplog) -> None: epochs = create_results(mocker, hyperopt, testdatadir) mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None) + mock_dump_json = mocker.patch('freqtrade.optimize.hyperopt.file_dump_json', return_value=None) results_file = testdatadir / 'optimize' / 'ut_results.pickle' caplog.set_level(logging.DEBUG) @@ -505,6 +318,7 @@ def test_save_results_saves_epochs(mocker, hyperopt, testdatadir, caplog) -> Non hyperopt._save_results() assert log_has(f"1 epoch saved to '{results_file}'.", caplog) mock_dump.assert_called_once() + mock_dump_json.assert_called_once() hyperopt.epochs = epochs + epochs hyperopt._save_results() @@ -521,6 +335,28 @@ def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> N mock_load.assert_called_once() +def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None: + epochs = create_results(mocker, hyperopt, testdatadir) + mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs) + mocker.patch.object(Path, 'is_file', MagicMock(return_value=True)) + statmock = MagicMock() + statmock.st_size = 5 + # mocker.patch.object(Path, 'stat', MagicMock(return_value=statmock)) + + results_file = testdatadir / 'optimize' / 'ut_results.pickle' + + hyperopt_epochs = hyperopt.load_previous_results(results_file) + + assert hyperopt_epochs == epochs + mock_load.assert_called_once() + + del epochs[0]['is_best'] + mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs) + + with pytest.raises(OperationalException): + hyperopt.load_previous_results(results_file) + + def test_roi_table_generation(hyperopt) -> None: params = { 'roi_t1': 5, @@ -536,6 +372,8 @@ def test_roi_table_generation(hyperopt) -> None: def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') + mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( @@ -839,19 +677,10 @@ def test_clean_hyperopt(mocker, hyperopt_conf, caplog): assert log_has(f"Removing `{h.data_pickle_file}`.", caplog) -def test_continue_hyperopt(mocker, hyperopt_conf, caplog): - patch_exchange(mocker) - hyperopt_conf.update({'hyperopt_continue': True}) - mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True)) - unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock()) - Hyperopt(hyperopt_conf) - - assert unlinkmock.call_count == 0 - assert log_has("Continuing on previous hyperopt results.", caplog) - - def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') + mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( @@ -907,6 +736,7 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None: def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( @@ -954,6 +784,7 @@ def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None: def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( @@ -1000,6 +831,7 @@ def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( @@ -1052,6 +884,7 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None: mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( @@ -1078,6 +911,7 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None: def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( @@ -1130,6 +964,7 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( @@ -1188,6 +1023,7 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: ]) def test_simplified_interface_failed(mocker, hyperopt_conf, method, space) -> None: mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( @@ -1207,3 +1043,39 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, method, space) -> No with pytest.raises(OperationalException, match=f"The '{space}' space is included into *"): hyperopt.start() + + +def test_print_epoch_details(capsys): + test_result = { + 'params_details': { + 'trailing': { + 'trailing_stop': True, + 'trailing_stop_positive': 0.02, + 'trailing_stop_positive_offset': 0.04, + 'trailing_only_offset_is_reached': True + }, + 'roi': { + 0: 0.18, + 90: 0.14, + 225: 0.05, + 430: 0}, + }, + 'results_explanation': 'foo result', + 'is_initial_point': False, + 'total_profit': 0, + 'current_epoch': 2, # This starts from 1 (in a human-friendly manner) + 'is_best': True + } + + Hyperopt.print_epoch_details(test_result, 5, False, no_header=True) + captured = capsys.readouterr() + assert '# Trailing stop:' in captured.out + # re.match(r"Pairs for .*", captured.out) + assert re.search(r'^\s+trailing_stop = True$', captured.out, re.MULTILINE) + assert re.search(r'^\s+trailing_stop_positive = 0.02$', captured.out, re.MULTILINE) + assert re.search(r'^\s+trailing_stop_positive_offset = 0.04$', captured.out, re.MULTILINE) + assert re.search(r'^\s+trailing_only_offset_is_reached = True$', captured.out, re.MULTILINE) + + assert '# ROI table:' in captured.out + assert re.search(r'^\s+minimal_roi = \{$', captured.out, re.MULTILINE) + assert re.search(r'^\s+\"90\"\:\s0.14,\s*$', captured.out, re.MULTILINE) diff --git a/tests/optimize/test_hyperoptloss.py b/tests/optimize/test_hyperoptloss.py new file mode 100644 index 000000000..63012ee48 --- /dev/null +++ b/tests/optimize/test_hyperoptloss.py @@ -0,0 +1,165 @@ +from datetime import datetime +from unittest.mock import MagicMock + +import pytest + +from freqtrade.exceptions import OperationalException +from freqtrade.optimize.default_hyperopt_loss import ShortTradeDurHyperOptLoss +from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver + + +def test_hyperoptlossresolver_noname(default_conf): + with pytest.raises(OperationalException, + match="No Hyperopt loss set. Please use `--hyperopt-loss` to specify " + "the Hyperopt-Loss class to use."): + HyperOptLossResolver.load_hyperoptloss(default_conf) + + +def test_hyperoptlossresolver(mocker, default_conf) -> None: + + hl = ShortTradeDurHyperOptLoss + mocker.patch( + 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object', + MagicMock(return_value=hl) + ) + default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) + x = HyperOptLossResolver.load_hyperoptloss(default_conf) + assert hasattr(x, "hyperopt_loss_function") + + +def test_hyperoptlossresolver_wrongname(default_conf) -> None: + default_conf.update({'hyperopt_loss': "NonExistingLossClass"}) + + with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'): + HyperOptLossResolver.load_hyperoptloss(default_conf) + + +def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_results) -> None: + hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, 600, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over > correct + assert under > correct + + +def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results) -> None: + resultsb = hyperopt_results.copy() + resultsb.loc[1, 'trade_duration'] = 20 + + hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) + longer = hl.hyperopt_loss_function(hyperopt_results, 100, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + shorter = hl.hyperopt_loss_function(resultsb, 100, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert shorter < longer + + +def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, 600, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, 600, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, 600, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + +def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'}) + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + +def test_sharpe_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + +def test_sortino_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'SortinoHyperOptLoss'}) + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + +def test_sortino_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'SortinoHyperOptLossDaily'}) + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + +def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'}) + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index b484e4390..d04929164 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -5,21 +5,17 @@ from pathlib import Path import pandas as pd import pytest from arrow import Arrow + from freqtrade.configuration import TimeRange from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.data import history -from freqtrade.data.btanalysis import (get_latest_backtest_filename, - load_backtest_data) +from freqtrade.data.btanalysis import get_latest_backtest_filename, load_backtest_data from freqtrade.edge import PairInfo -from freqtrade.optimize.optimize_reports import (generate_backtest_stats, - generate_daily_stats, - generate_edge_table, - generate_pair_metrics, +from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats, + generate_edge_table, generate_pair_metrics, generate_sell_reason_stats, - generate_strategy_metrics, - store_backtest_stats, - text_table_bt_results, - text_table_sell_reason, + generate_strategy_metrics, store_backtest_stats, + text_table_bt_results, text_table_sell_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.strategy.interface import SellType diff --git a/tests/pairlist/test_pairlocks.py b/tests/pairlist/test_pairlocks.py new file mode 100644 index 000000000..0b6b89717 --- /dev/null +++ b/tests/pairlist/test_pairlocks.py @@ -0,0 +1,82 @@ +from datetime import datetime, timedelta, timezone + +import arrow +import pytest + +from freqtrade.persistence import PairLocks +from freqtrade.persistence.models import PairLock + + +@pytest.mark.parametrize('use_db', (False, True)) +@pytest.mark.usefixtures("init_persistence") +def test_PairLocks(use_db): + PairLocks.timeframe = '5m' + # No lock should be present + if use_db: + assert len(PairLock.query.all()) == 0 + else: + PairLocks.use_db = False + + assert PairLocks.use_db == use_db + + pair = 'ETH/BTC' + assert not PairLocks.is_pair_locked(pair) + PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + # ETH/BTC locked for 4 minutes + assert PairLocks.is_pair_locked(pair) + + # XRP/BTC should not be locked now + pair = 'XRP/BTC' + assert not PairLocks.is_pair_locked(pair) + # Unlocking a pair that's not locked should not raise an error + PairLocks.unlock_pair(pair) + + PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + assert PairLocks.is_pair_locked(pair) + + # Get both locks from above + locks = PairLocks.get_pair_locks(None) + assert len(locks) == 2 + + # Unlock original pair + pair = 'ETH/BTC' + PairLocks.unlock_pair(pair) + assert not PairLocks.is_pair_locked(pair) + assert not PairLocks.is_global_lock() + + pair = 'BTC/USDT' + # Lock until 14:30 + lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) + PairLocks.lock_pair(pair, lock_time) + + assert not PairLocks.is_pair_locked(pair) + assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-10)) + assert not PairLocks.is_global_lock(lock_time + timedelta(minutes=-10)) + assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-50)) + assert not PairLocks.is_global_lock(lock_time + timedelta(minutes=-50)) + + # Should not be locked after time expired + assert not PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=10)) + + locks = PairLocks.get_pair_locks(pair, lock_time + timedelta(minutes=-2)) + assert len(locks) == 1 + assert 'PairLock' in str(locks[0]) + + # Unlock all + PairLocks.unlock_pair(pair, lock_time + timedelta(minutes=-2)) + assert not PairLocks.is_global_lock(lock_time + timedelta(minutes=-50)) + + # Global lock + PairLocks.lock_pair('*', lock_time) + assert PairLocks.is_global_lock(lock_time + timedelta(minutes=-50)) + # Global lock also locks every pair seperately + assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-50)) + assert PairLocks.is_pair_locked('XRP/USDT', lock_time + timedelta(minutes=-50)) + + if use_db: + assert len(PairLock.query.all()) > 0 + else: + # Nothing was pushed to the database + assert len(PairLock.query.all()) == 0 + # Reset use-db variable + PairLocks.use_db = True diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index c2dee6439..977dfbc20 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -13,8 +13,7 @@ from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from tests.conftest import (create_mock_trades, get_patched_freqtradebot, - patch_get_signal) +from tests.conftest import create_mock_trades, get_patched_freqtradebot, patch_get_signal # Functions for recurrent object patching diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 626586a4a..7b4e2e153 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -2,7 +2,8 @@ Unit test file for rpc/api_server.py """ -from datetime import datetime +from datetime import datetime, timedelta, timezone +from pathlib import Path from unittest.mock import ANY, MagicMock, PropertyMock import pytest @@ -11,11 +12,11 @@ from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ from freqtrade.loggers import setup_logging, setup_logging_pre -from freqtrade.persistence import Trade +from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc.api_server import BASE_URI, ApiServer from freqtrade.state import State -from tests.conftest import (create_mock_trades, get_patched_freqtradebot, - log_has, patch_get_signal) +from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal + _TEST_USER = "FreqTrader" _TEST_PASS = "SuperSecurePassword1!" @@ -327,6 +328,30 @@ def test_api_count(botclient, mocker, ticker, fee, markets): assert rc.json["max"] == 1.0 +def test_api_locks(botclient): + ftbot, client = botclient + + rc = client_get(client, f"{BASE_URI}/locks") + assert_response(rc) + + assert 'locks' in rc.json + + assert rc.json['lock_count'] == 0 + assert rc.json['lock_count'] == len(rc.json['locks']) + + PairLocks.lock_pair('ETH/BTC', datetime.now(timezone.utc) + timedelta(minutes=4), 'randreason') + PairLocks.lock_pair('XRP/BTC', datetime.now(timezone.utc) + timedelta(minutes=20), 'deadbeef') + + rc = client_get(client, f"{BASE_URI}/locks") + assert_response(rc) + + assert rc.json['lock_count'] == 2 + assert rc.json['lock_count'] == len(rc.json['locks']) + assert 'ETH/BTC' in (rc.json['locks'][0]['pair'], rc.json['locks'][1]['pair']) + assert 'randreason' in (rc.json['locks'][0]['reason'], rc.json['locks'][1]['reason']) + assert 'deadbeef' in (rc.json['locks'][0]['reason'], rc.json['locks'][1]['reason']) + + def test_api_show_config(botclient, mocker): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) @@ -811,3 +836,176 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): data='{"tradeid": "1"}') assert_response(rc) assert rc.json == {'result': 'Created sell order for trade 1.'} + + +def test_api_pair_candles(botclient, ohlcv_history): + ftbot, client = botclient + timeframe = '5m' + amount = 2 + + # No pair + rc = client_get(client, + f"{BASE_URI}/pair_candles?limit={amount}&timeframe={timeframe}") + assert_response(rc, 400) + + # No timeframe + rc = client_get(client, + f"{BASE_URI}/pair_candles?pair=XRP%2FBTC") + assert_response(rc, 400) + + rc = client_get(client, + f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") + assert_response(rc) + assert 'columns' in rc.json + assert 'data_start_ts' in rc.json + assert 'data_start' in rc.json + assert 'data_stop' in rc.json + assert 'data_stop_ts' in rc.json + assert len(rc.json['data']) == 0 + ohlcv_history['sma'] = ohlcv_history['close'].rolling(2).mean() + ohlcv_history['buy'] = 0 + ohlcv_history.loc[1, 'buy'] = 1 + ohlcv_history['sell'] = 0 + + ftbot.dataprovider._set_cached_df("XRP/BTC", timeframe, ohlcv_history) + + rc = client_get(client, + f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") + assert_response(rc) + assert 'strategy' in rc.json + assert rc.json['strategy'] == 'DefaultStrategy' + assert 'columns' in rc.json + assert 'data_start_ts' in rc.json + assert 'data_start' in rc.json + assert 'data_stop' in rc.json + assert 'data_stop_ts' in rc.json + assert rc.json['data_start'] == '2017-11-26 08:50:00+00:00' + assert rc.json['data_start_ts'] == 1511686200000 + assert rc.json['data_stop'] == '2017-11-26 08:55:00+00:00' + assert rc.json['data_stop_ts'] == 1511686500000 + assert isinstance(rc.json['columns'], list) + assert rc.json['columns'] == ['date', 'open', 'high', + 'low', 'close', 'volume', 'sma', 'buy', 'sell', + '__date_ts', '_buy_signal_open', '_sell_signal_open'] + assert 'pair' in rc.json + assert rc.json['pair'] == 'XRP/BTC' + + assert 'data' in rc.json + assert len(rc.json['data']) == amount + + assert (rc.json['data'] == + [['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, + None, 0, 0, 1511686200000, None, None], + ['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05, + 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.88e-05, None] + ]) + + +def test_api_pair_history(botclient, ohlcv_history): + ftbot, client = botclient + timeframe = '5m' + + # No pair + rc = client_get(client, + f"{BASE_URI}/pair_history?timeframe={timeframe}" + "&timerange=20180111-20180112&strategy=DefaultStrategy") + assert_response(rc, 400) + + # No Timeframe + rc = client_get(client, + f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC" + "&timerange=20180111-20180112&strategy=DefaultStrategy") + assert_response(rc, 400) + + # No timerange + rc = client_get(client, + f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" + "&strategy=DefaultStrategy") + assert_response(rc, 400) + + # No strategy + rc = client_get(client, + f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" + "&timerange=20180111-20180112") + assert_response(rc, 400) + + # Working + rc = client_get(client, + f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" + "&timerange=20180111-20180112&strategy=DefaultStrategy") + assert_response(rc, 200) + assert rc.json['length'] == 289 + assert len(rc.json['data']) == rc.json['length'] + assert 'columns' in rc.json + assert 'data' in rc.json + assert rc.json['pair'] == 'UNITTEST/BTC' + assert rc.json['strategy'] == 'DefaultStrategy' + assert rc.json['data_start'] == '2018-01-11 00:00:00+00:00' + assert rc.json['data_start_ts'] == 1515628800000 + assert rc.json['data_stop'] == '2018-01-12 00:00:00+00:00' + assert rc.json['data_stop_ts'] == 1515715200000 + + +def test_api_plot_config(botclient): + ftbot, client = botclient + + rc = client_get(client, f"{BASE_URI}/plot_config") + assert_response(rc) + assert rc.json == {} + + ftbot.strategy.plot_config = {'main_plot': {'sma': {}}, + 'subplots': {'RSI': {'rsi': {'color': 'red'}}}} + rc = client_get(client, f"{BASE_URI}/plot_config") + assert_response(rc) + assert rc.json == ftbot.strategy.plot_config + assert isinstance(rc.json['main_plot'], dict) + + +def test_api_strategies(botclient): + ftbot, client = botclient + + rc = client_get(client, f"{BASE_URI}/strategies") + + assert_response(rc) + assert rc.json == {'strategies': ['DefaultStrategy', 'TestStrategyLegacy']} + + +def test_api_strategy(botclient): + ftbot, client = botclient + + rc = client_get(client, f"{BASE_URI}/strategy/DefaultStrategy") + + assert_response(rc) + assert rc.json['strategy'] == 'DefaultStrategy' + + data = (Path(__file__).parents[1] / "strategy/strats/default_strategy.py").read_text() + assert rc.json['code'] == data + + rc = client_get(client, f"{BASE_URI}/strategy/NoStrat") + assert_response(rc, 404) + + +def test_list_available_pairs(botclient): + ftbot, client = botclient + + rc = client_get(client, f"{BASE_URI}/available_pairs") + + assert_response(rc) + assert rc.json['length'] == 12 + assert isinstance(rc.json['pairs'], list) + + rc = client_get(client, f"{BASE_URI}/available_pairs?timeframe=5m") + assert_response(rc) + assert rc.json['length'] == 12 + + rc = client_get(client, f"{BASE_URI}/available_pairs?stake_currency=ETH") + assert_response(rc) + assert rc.json['length'] == 1 + assert rc.json['pairs'] == ['XRP/ETH'] + assert len(rc.json['pair_interval']) == 2 + + rc = client_get(client, f"{BASE_URI}/available_pairs?stake_currency=ETH&timeframe=5m") + assert_response(rc) + assert rc.json['length'] == 1 + assert rc.json['pairs'] == ['XRP/ETH'] + assert len(rc.json['pair_interval']) == 1 diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index e8d0f648e..4b715fc37 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -1,10 +1,10 @@ # pragma pylint: disable=missing-docstring, C0103 -import time import logging +import time from unittest.mock import MagicMock -from freqtrade.rpc import RPCMessageType, RPCManager -from tests.conftest import log_has, get_patched_freqtradebot +from freqtrade.rpc import RPCManager, RPCMessageType +from tests.conftest import get_patched_freqtradebot, log_has def test__init__(mocker, default_conf) -> None: diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 3958a825a..f1246005f 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -18,14 +18,13 @@ from freqtrade.constants import CANCEL_REASON from freqtrade.edge import PairInfo from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging -from freqtrade.persistence import Trade +from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State from freqtrade.strategy.interface import SellType -from tests.conftest import (create_mock_trades, get_patched_freqtradebot, - log_has, patch_exchange, patch_get_signal, - patch_whitelist) +from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, patch_exchange, + patch_get_signal, patch_whitelist) class DummyCls(Telegram): @@ -76,14 +75,14 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " - "['delete'], ['performance'], ['daily'], ['count'], ['reload_config', " - "'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " + "['delete'], ['performance'], ['daily'], ['count'], ['locks'], " + "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']]") assert log_has(message_str, caplog) -def test_cleanup(default_conf, mocker) -> None: +def test_cleanup(default_conf, mocker, ) -> None: updater_mock = MagicMock() updater_mock.stop = MagicMock() mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock) @@ -93,13 +92,9 @@ def test_cleanup(default_conf, mocker) -> None: assert telegram._updater.stop.call_count == 1 -def test_authorized_only(default_conf, mocker, caplog) -> None: +def test_authorized_only(default_conf, mocker, caplog, update) -> None: patch_exchange(mocker) - chat = Chat(0, 0) - update = Update(randint(1, 100)) - update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) - default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) patch_get_signal(bot, (True, False)) @@ -115,7 +110,7 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: patch_exchange(mocker) chat = Chat(0xdeadbeef, 0) update = Update(randint(1, 100)) - update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) + update.message = Message(randint(1, 100), datetime.utcnow(), chat) default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) @@ -128,12 +123,9 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: assert not log_has('Exception occurred within Telegram module', caplog) -def test_authorized_only_exception(default_conf, mocker, caplog) -> None: +def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None: patch_exchange(mocker) - update = Update(randint(1, 100)) - update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0)) - default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) @@ -147,7 +139,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None: assert log_has('Exception occurred within Telegram module', caplog) -def test_telegram_status(default_conf, update, mocker, fee, ticker,) -> None: +def test_telegram_status(default_conf, update, mocker) -> None: update.message.chat.id = "123" default_conf['telegram']['enabled'] = False default_conf['telegram']['chat_id'] = "123" @@ -1032,6 +1024,43 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: assert msg in msg_mock.call_args_list[0][0][0] +def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None: + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + get_fee=fee, + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) + telegram = Telegram(freqtradebot) + + freqtradebot.state = State.STOPPED + telegram._locks(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + assert 'not running' in msg_mock.call_args_list[0][0][0] + msg_mock.reset_mock() + freqtradebot.state = State.RUNNING + + PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason') + PairLocks.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef') + + telegram._locks(update=update, context=MagicMock()) + + assert 'Pair' in msg_mock.call_args_list[0][0][0] + assert 'Until' in msg_mock.call_args_list[0][0][0] + assert 'Reason\n' in msg_mock.call_args_list[0][0][0] + assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0] + assert 'XRP/BTC' in msg_mock.call_args_list[0][0][0] + assert 'deadbeef' in msg_mock.call_args_list[0][0][0] + assert 'randreason' in msg_mock.call_args_list[0][0][0] + + def test_whitelist_static(default_conf, update, mocker) -> None: msg_mock = MagicMock() mocker.patch.multiple( @@ -1139,9 +1168,9 @@ def test_telegram_logs(default_conf, update, mocker) -> None: context = MagicMock() context.args = [] telegram._logs(update=update, context=context) - # Called at least 3 times. Exact times will change with unrelated changes to setup messages + # Called at least 2 times. Exact times will change with unrelated changes to setup messages # Therefore we don't test for this explicitly. - assert msg_mock.call_count > 3 + assert msg_mock.call_count >= 2 def test_edge_disabled(default_conf, update, mocker) -> None: diff --git a/tests/strategy/strats/legacy_strategy.py b/tests/strategy/strats/legacy_strategy.py index 9cbce0ad5..1e7bb5e1e 100644 --- a/tests/strategy/strats/legacy_strategy.py +++ b/tests/strategy/strats/legacy_strategy.py @@ -1,12 +1,13 @@ # --- Do not remove these libs --- -from freqtrade.strategy.interface import IStrategy -from pandas import DataFrame -# -------------------------------- - # Add your lib to import here import talib.abstract as ta +from pandas import DataFrame +from freqtrade.strategy.interface import IStrategy + + +# -------------------------------- # This class is a sample. Feel free to customize it. class TestStrategyLegacy(IStrategy): diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index f1b5d0244..7cf9a0624 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, C0103 - import logging from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock @@ -12,13 +11,14 @@ from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data from freqtrade.exceptions import StrategyError -from freqtrade.persistence import Trade +from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re from .strats.default_strategy import DefaultStrategy + # Avoid to reinit the same object again and again _STRATEGY = DefaultStrategy(config={}) _STRATEGY.dp = DataProvider({}, None, None) @@ -359,23 +359,20 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> assert log_has('Skipping TA Analysis for already analyzed candle', caplog) +@pytest.mark.usefixtures("init_persistence") def test_is_pair_locked(default_conf): default_conf.update({'strategy': 'DefaultStrategy'}) + PairLocks.timeframe = default_conf['timeframe'] strategy = StrategyResolver.load_strategy(default_conf) - # dict should be empty - assert not strategy._pair_locked_until + # No lock should be present + assert len(PairLocks.get_pair_locks(None)) == 0 pair = 'ETH/BTC' assert not strategy.is_pair_locked(pair) - strategy.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + strategy.lock_pair(pair, arrow.now(timezone.utc).shift(minutes=4).datetime) # ETH/BTC locked for 4 minutes assert strategy.is_pair_locked(pair) - # Test lock does not change - lock = strategy._pair_locked_until[pair] - strategy.lock_pair(pair, arrow.utcnow().shift(minutes=2).datetime) - assert lock == strategy._pair_locked_until[pair] - # XRP/BTC should not be locked now pair = 'XRP/BTC' assert not strategy.is_pair_locked(pair) @@ -391,8 +388,9 @@ def test_is_pair_locked(default_conf): pair = 'BTC/USDT' # Lock until 14:30 lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) - strategy.lock_pair(pair, lock_time) - # Lock is in the past ... + # Subtract 2 seconds, as locking rounds up to the next candle. + strategy.lock_pair(pair, lock_time - timedelta(seconds=2)) + assert not strategy.is_pair_locked(pair) # latest candle is from 14:20, lock goes to 14:30 assert strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-10)) diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 240f3d8ec..1c692d2da 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -18,13 +18,15 @@ def test_search_strategy(): s, _ = StrategyResolver._search_object( directory=default_location, - object_name='DefaultStrategy' + object_name='DefaultStrategy', + add_source=True, ) assert issubclass(s, IStrategy) s, _ = StrategyResolver._search_object( directory=default_location, - object_name='NotFoundStrategy' + object_name='NotFoundStrategy', + add_source=True, ) assert s is None @@ -53,6 +55,9 @@ def test_load_strategy(default_conf, result): 'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates') }) strategy = StrategyResolver.load_strategy(default_conf) + assert isinstance(strategy.__source__, str) + assert 'class SampleStrategy' in strategy.__source__ + assert isinstance(strategy.__file__, str) assert 'rsi' in strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 4b29bf304..1d3e80d24 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -1,5 +1,5 @@ -import pandas as pd import numpy as np +import pandas as pd from freqtrade.strategy import merge_informative_pair, timeframe_to_minutes diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 2af36277b..e2a1ae53c 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock import pytest from freqtrade.commands import Arguments -from freqtrade.commands.cli_options import check_int_positive +from freqtrade.commands.cli_options import check_int_nonzero, check_int_positive # Parse common command-line-arguments. Used for all tools @@ -249,8 +249,31 @@ def test_check_int_positive() -> None: with pytest.raises(argparse.ArgumentTypeError): check_int_positive('0') + with pytest.raises(argparse.ArgumentTypeError): + check_int_positive(0) + with pytest.raises(argparse.ArgumentTypeError): check_int_positive('3.5') with pytest.raises(argparse.ArgumentTypeError): check_int_positive('DeadBeef') + + +def test_check_int_nonzero() -> None: + assert check_int_nonzero('3') == 3 + assert check_int_nonzero('1') == 1 + assert check_int_nonzero('100') == 100 + + assert check_int_nonzero('-2') == -2 + + with pytest.raises(argparse.ArgumentTypeError): + check_int_nonzero('0') + + with pytest.raises(argparse.ArgumentTypeError): + check_int_nonzero(0) + + with pytest.raises(argparse.ArgumentTypeError): + check_int_nonzero('3.5') + + with pytest.raises(argparse.ArgumentTypeError): + check_int_nonzero('DeadBeef') diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 4428fe240..7d6c81f74 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -11,20 +11,18 @@ import pytest from jsonschema import ValidationError from freqtrade.commands import Arguments -from freqtrade.configuration import (Configuration, check_exchange, - remove_credentials, +from freqtrade.configuration import (Configuration, check_exchange, remove_credentials, validate_config_consistency) from freqtrade.configuration.config_validation import validate_config_schema -from freqtrade.configuration.deprecated_settings import ( - check_conflicting_settings, process_deprecated_setting, - process_temporary_deprecated_settings) +from freqtrade.configuration.deprecated_settings import (check_conflicting_settings, + process_deprecated_setting, + process_temporary_deprecated_settings) from freqtrade.configuration.load_config import load_config_file, log_config_error_range from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.exceptions import OperationalException from freqtrade.loggers import _set_loggers, setup_logging, setup_logging_pre from freqtrade.state import RunMode -from tests.conftest import (log_has, log_has_re, - patched_configuration_load_config_file) +from tests.conftest import log_has, log_has_re, patched_configuration_load_config_file @pytest.fixture(scope="function") diff --git a/tests/test_directory_operations.py b/tests/test_directory_operations.py index 71c91549f..a8058c514 100644 --- a/tests/test_directory_operations.py +++ b/tests/test_directory_operations.py @@ -4,8 +4,7 @@ from unittest.mock import MagicMock import pytest -from freqtrade.configuration.directory_operations import (copy_sample_files, - create_datadir, +from freqtrade.configuration.directory_operations import (copy_sample_files, create_datadir, create_userdata_dir) from freqtrade.exceptions import OperationalException from tests.conftest import log_has, log_has_re diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0c12c05bb..1f5b3ecaa 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -10,28 +10,23 @@ from unittest.mock import ANY, MagicMock, PropertyMock import arrow import pytest -from freqtrade.constants import (CANCEL_REASON, MATH_CLOSE_PREC, - UNLIMITED_STAKE_AMOUNT) -from freqtrade.exceptions import (DependencyException, ExchangeError, - InsufficientFundsError, - InvalidOrderException, OperationalException, - PricingError, TemporaryError) +from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT +from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, + InvalidOrderException, OperationalException, PricingError, + TemporaryError) from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import Trade -from freqtrade.persistence.models import Order +from freqtrade.persistence import Order, Trade +from freqtrade.persistence.models import PairLock from freqtrade.rpc import RPCMessageType from freqtrade.state import RunMode, State from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.worker import Worker -from tests.conftest import (create_mock_trades, get_patched_freqtradebot, - get_patched_worker, log_has, log_has_re, - patch_edge, patch_exchange, patch_get_signal, +from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, + log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, patch_wallet, patch_whitelist) -from tests.conftest_trades import (MOCK_TRADE_COUNT, mock_order_1, - mock_order_2, mock_order_2_sell, - mock_order_3, mock_order_3_sell, - mock_order_4, mock_order_5_stoploss, - mock_order_6_sell) +from tests.conftest_trades import (MOCK_TRADE_COUNT, mock_order_1, mock_order_2, mock_order_2_sell, + mock_order_3, mock_order_3_sell, mock_order_4, + mock_order_5_stoploss, mock_order_6_sell) def patch_RPCManager(mocker) -> MagicMock: @@ -71,7 +66,7 @@ def test_process_stopped(mocker, default_conf) -> None: def test_bot_cleanup(mocker, default_conf, caplog) -> None: - mock_cleanup = mocker.patch('freqtrade.persistence.cleanup') + mock_cleanup = mocker.patch('freqtrade.freqtradebot.cleanup_db') coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders') freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade.cleanup() @@ -2805,6 +2800,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c trade = Trade.query.first() Trade.session = MagicMock() + PairLock.session = MagicMock() freqtrade.config['dry_run'] = False trade.stoploss_order_id = "abcd" @@ -3255,7 +3251,6 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], sell_reason=SellType.STOP_LOSS) trade.close(ticker_sell_down()['bid']) - assert trade.pair in freqtrade.strategy._pair_locked_until assert freqtrade.strategy.is_pair_locked(trade.pair) # reinit - should buy other pair. diff --git a/tests/test_indicators.py b/tests/test_indicators.py index 2f9bdc0f9..8d02330a1 100644 --- a/tests/test_indicators.py +++ b/tests/test_indicators.py @@ -1,7 +1,8 @@ -import freqtrade.vendor.qtpylib.indicators as qtpylib import numpy as np import pandas as pd +import freqtrade.vendor.qtpylib.indicators as qtpylib + def test_crossed_numpy_types(): """ diff --git a/tests/test_main.py b/tests/test_main.py index dd0c877e8..f55aea336 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,11 +1,11 @@ # pragma pylint: disable=missing-docstring from copy import deepcopy +from pathlib import Path from unittest.mock import MagicMock, PropertyMock import pytest -from pathlib import Path from freqtrade.commands import Arguments from freqtrade.exceptions import FreqtradeException, OperationalException from freqtrade.freqtradebot import FreqtradeBot @@ -65,7 +65,7 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=Exception)) patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) + mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) args = ['trade', '-c', 'config.json.example'] @@ -83,7 +83,7 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) + mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) args = ['trade', '-c', 'config.json.example'] @@ -104,7 +104,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) + mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) args = ['trade', '-c', 'config.json.example'] @@ -155,7 +155,7 @@ def test_main_reload_config(mocker, default_conf, caplog) -> None: reconfigure_mock = mocker.patch('freqtrade.worker.Worker._reconfigure', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) + mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg() worker = Worker(args=args, config=default_conf) @@ -178,7 +178,7 @@ def test_reconfigure(mocker, default_conf) -> None: mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) + mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg() worker = Worker(args=args, config=default_conf) diff --git a/tests/test_misc.py b/tests/test_misc.py index a185cbba4..6dcd9fbe5 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -7,9 +7,8 @@ from unittest.mock import MagicMock import pytest from freqtrade.data.converter import ohlcv_to_dataframe -from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json, - file_load_json, format_ms_time, pair_to_filename, - plural, render_template, +from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json, file_load_json, + format_ms_time, pair_to_filename, plural, render_template, render_template_with_fallback, safe_value_fallback, safe_value_fallback2, shorten_date) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index adfa18876..4216565ac 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -8,13 +8,13 @@ from sqlalchemy import create_engine from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.persistence import Order, Trade, clean_dry_run_db, init +from freqtrade.persistence import Order, Trade, clean_dry_run_db, init_db from tests.conftest import create_mock_trades, log_has, log_has_re def test_init_create_session(default_conf): # Check if init create a session - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert hasattr(Trade, 'session') assert 'scoped_session' in type(Trade.session).__name__ @@ -24,7 +24,7 @@ def test_init_custom_db_url(default_conf, mocker): default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite' @@ -33,7 +33,7 @@ def test_init_invalid_db_url(default_conf): # Update path to a value other than default, but still in-memory default_conf.update({'db_url': 'unknown:///some.url'}) with pytest.raises(OperationalException, match=r'.*no valid database URL*'): - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) def test_init_prod_db(default_conf, mocker): @@ -42,7 +42,7 @@ def test_init_prod_db(default_conf, mocker): create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite' @@ -53,7 +53,7 @@ def test_init_dryrun_db(default_conf, mocker): create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.dryrun.sqlite' @@ -482,7 +482,7 @@ def test_migrate_old(mocker, default_conf, fee): engine.execute(insert_table_old) engine.execute(insert_table_old2) # Run init to test migration - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -581,7 +581,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): engine.execute("create table trades_bak1 as select * from trades") # Run init to test migration - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -661,7 +661,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): engine.execute(insert_table_old) # Run init to test migration - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -904,7 +904,7 @@ def test_to_json(default_conf, fee): def test_stoploss_reinitialization(default_conf, fee): - init(default_conf['db_url']) + init_db(default_conf['db_url']) trade = Trade( pair='ETH/BTC', stake_amount=0.001, diff --git a/tests/test_plotting.py b/tests/test_plotting.py index bcababbf1..401f66b60 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -13,13 +13,10 @@ from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data from freqtrade.exceptions import OperationalException -from freqtrade.plot.plotting import (add_indicators, add_profit, - create_plotconfig, - generate_candlestick_graph, - generate_plot_filename, - generate_profit_graph, init_plotscript, - load_and_plot_trades, plot_profit, - plot_trades, store_plot_file) +from freqtrade.plot.plotting import (add_indicators, add_profit, create_plotconfig, + generate_candlestick_graph, generate_plot_filename, + generate_profit_graph, init_plotscript, load_and_plot_trades, + plot_profit, plot_trades, store_plot_file) from freqtrade.resolvers import StrategyResolver from tests.conftest import get_args, log_has, log_has_re, patch_exchange diff --git a/tests/test_talib.py b/tests/test_talib.py index 4effc129b..f526fdd4d 100644 --- a/tests/test_talib.py +++ b/tests/test_talib.py @@ -1,5 +1,5 @@ -import talib.abstract as ta import pandas as pd +import talib.abstract as ta def test_talib_bollingerbands_near_zero_values(): diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 450dabc4d..b7aead0c4 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -74,6 +74,10 @@ def test_sync_wallet_at_boot(mocker, default_conf): freqtrade.wallets.update() assert update_mock.call_count == 1 + assert freqtrade.wallets.get_free('NOCURRENCY') == 0 + assert freqtrade.wallets.get_used('NOCURRENCY') == 0 + assert freqtrade.wallets.get_total('NOCURRENCY') == 0 + def test_sync_wallet_missing_data(mocker, default_conf): default_conf['dry_run'] = False