diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6a111944..53b2e5440 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,19 +64,17 @@ jobs: pip install -e . - name: Tests - env: - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} - COVERALLS_SERVICE_NAME: travis-ci - TRAVIS: "true" run: | pytest --random-order --cov=freqtrade --cov-config=.coveragerc + + - name: Coveralls + if: startsWith(matrix.os, 'ubuntu') + env: + # Coveralls token. Not used as secret due to github not providing secrets to forked repositories + COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu + run: | # Allow failure for coveralls - # Fake travis environment to get coveralls working correctly - export TRAVIS_PULL_REQUEST="https://github.com/${GITHUB_REPOSITORY}/pull/$(cat $GITHUB_EVENT_PATH | jq -r .number)" - export TRAVIS_BRANCH=${GITHUB_REF#"ref/heads"} - export CI_BRANCH=${GITHUB_REF#"ref/heads"} - echo "${TRAVIS_BRANCH}" - coveralls || true + coveralls -v || true - name: Backtesting run: | diff --git a/Dockerfile b/Dockerfile index dc9b04403..f631d891d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7.5-slim-stretch +FROM python:3.7.6-slim-stretch RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev \ diff --git a/docs/backtesting.md b/docs/backtesting.md index 017289905..ac7c8e11a 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -137,12 +137,12 @@ A backtesting result will look like that: | ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 | 15 | | TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 | ========================================================= SELL REASON STATS ========================================================= -| Sell Reason | Count | -|:-------------------|--------:| -| trailing_stop_loss | 205 | -| stop_loss | 166 | -| sell_signal | 56 | -| force_sell | 2 | +| Sell Reason | Count | Profit | Loss | +|:-------------------|--------:|---------:|-------:| +| trailing_stop_loss | 205 | 150 | 55 | +| stop_loss | 166 | 0 | 166 | +| sell_signal | 56 | 36 | 20 | +| force_sell | 2 | 0 | 2 | ====================================================== LEFT OPEN TRADES REPORT ====================================================== | pair | buy count | avg profit % | cum profit % | tot profit BTC | tot profit % | avg duration | profit | loss | |:---------|------------:|---------------:|---------------:|-----------------:|---------------:|:---------------|---------:|-------:| @@ -154,6 +154,7 @@ A backtesting result will look like that: The 1st table contains all trades the bot made, including "left open trades". The 2nd table contains a recap of sell reasons. +This table can tell you which area needs some additional work (i.e. all `sell_signal` trades are losses, so we should disable the sell-signal or work on improving that). The 3rd table contains all trades the bot had to `forcesell` at the end of the backtest period to present a full picture. This is necessary to simulate realistic behaviour, since the backtest period has to end at some point, while realistically, you could leave the bot running forever. diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 25818aea6..e856755d2 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -45,14 +45,17 @@ optional arguments: -h, --help show this help message and exit --db-url PATH Override trades database URL, this is useful in custom deployments (default: `sqlite:///tradesv3.sqlite` for - Live Run mode, `sqlite://` for Dry Run). + Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for + Dry Run). --sd-notify Notify systemd service manager. --dry-run Enforce dry-run for trading (removes Exchange secrets and simulates trades). Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. + --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: `config.json`). @@ -68,6 +71,7 @@ Strategy arguments: Specify strategy class name which will be used by the bot. --strategy-path PATH Specify additional strategy lookup path. + ``` ### How to specify which configuration file be used? @@ -192,8 +196,8 @@ Backtesting also uses the config specified via `-c/--config`. usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [-i TICKER_INTERVAL] - [--timerange TIMERANGE] [--max_open_trades INT] - [--stake_amount STAKE_AMOUNT] [--fee FLOAT] + [--timerange TIMERANGE] [--max-open-trades INT] + [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--eps] [--dmmp] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--export EXPORT] [--export-filename PATH] @@ -205,10 +209,12 @@ optional arguments: `1d`). --timerange TIMERANGE Specify what timerange of data to use. - --max_open_trades INT - Specify max_open_trades to use. - --stake_amount STAKE_AMOUNT - Specify stake_amount. + --max-open-trades INT + Override the value of the `max_open_trades` + configuration setting. + --stake-amount STAKE_AMOUNT + Override the value of the `stake_amount` configuration + setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). --eps, --enable-position-stacking @@ -270,8 +276,8 @@ to find optimal parameter values for your stategy. usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [-i TICKER_INTERVAL] [--timerange TIMERANGE] - [--max_open_trades INT] - [--stake_amount STAKE_AMOUNT] [--fee FLOAT] + [--max-open-trades INT] + [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--hyperopt NAME] [--hyperopt-path PATH] [--eps] [-e INT] [--spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] @@ -286,10 +292,12 @@ optional arguments: `1d`). --timerange TIMERANGE Specify what timerange of data to use. - --max_open_trades INT - Specify max_open_trades to use. - --stake_amount STAKE_AMOUNT - Specify stake_amount. + --max-open-trades INT + Override the value of the `max_open_trades` + configuration setting. + --stake-amount STAKE_AMOUNT + Override the value of the `stake_amount` configuration + setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). --hyperopt NAME Specify hyperopt class name which will be used by the @@ -360,7 +368,7 @@ To know your trade expectancy and winrate against historical data, you can use E usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [-i TICKER_INTERVAL] [--timerange TIMERANGE] - [--max_open_trades INT] [--stake_amount STAKE_AMOUNT] + [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--stoplosses STOPLOSS_RANGE] optional arguments: @@ -370,10 +378,12 @@ optional arguments: `1d`). --timerange TIMERANGE Specify what timerange of data to use. - --max_open_trades INT - Specify max_open_trades to use. - --stake_amount STAKE_AMOUNT - Specify stake_amount. + --max-open-trades INT + Override the value of the `max_open_trades` + configuration setting. + --stake-amount STAKE_AMOUNT + Override the value of the `stake_amount` configuration + setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). --stoplosses STOPLOSS_RANGE diff --git a/docs/configuration.md b/docs/configuration.md index 73534b6f1..90f0aa791 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -38,8 +38,8 @@ The prevelance for all Options is as follows: Mandatory parameters are marked as **Required**, which means that they are required to be set in one of the possible ways. -| Command | Description | -|----------|-------------| +| Parameter | Description | +|------------|-------------| | `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades).
***Datatype:*** *Positive integer or -1.* | `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *String* | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#understand-stake_amount). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Positive float or `"unlimited"`.* @@ -55,14 +55,14 @@ 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). [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). [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.
***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.
***Datatype:*** *Integer* -| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance). -| `bid_strategy.use_order_book` | Enable buying using the rates in Order Book Bids.
***Datatype:*** *Boolean* -| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. *Defaults to `1`.*
***Datatype:*** *Positive Integer* -| `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book.
*Defaults to `false`.*
***Datatype:*** *Boolean* -| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher. *Defaults to `0`.*
***Datatype:*** *Float (as ratio)* -| `ask_strategy.use_order_book` | Enable selling of open trades using Order Book Asks.
***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* +| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook). +| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled).
***Datatype:*** *Boolean* +| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled).
*Defaults to `1`.*
***Datatype:*** *Positive Integer* +| `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market).
*Defaults to `false`.*
***Datatype:*** *Boolean* +| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market)
*Defaults to `0`.*
***Datatype:*** *Float (as ratio)* +| `ask_strategy.use_order_book` | Enable selling of open trades using [Order Book Asks](#sell-price-with-orderbook-enabled).
***Datatype:*** *Boolean* | `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
*Defaults to `1`.*
***Datatype:*** *Positive Integer* | `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
*Defaults to `1`.*
***Datatype:*** *Positive Integer* | `ask_strategy.use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `true`.*
***Datatype:*** *Boolean* @@ -72,9 +72,9 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Dict* | `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
***Datatype:*** *String* | `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.
***Datatype:*** *Boolean* -| `exchange.key` | API key to use for the exchange. Only required when you are in production mode. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* -| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* -| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.
**Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* | `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#dynamic-pairlists)).
***Datatype:*** *List* | `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#dynamic-pairlists)).
***Datatype:*** *List* | `exchange.ccxt_config` | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
***Datatype:*** *Dict* @@ -84,19 +84,19 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
*Defaults to `true`.*
***Datatype:*** *Boolean* | `pairlists` | Define one or more pairlists to be used. [More information below](#dynamic-pairlists).
*Defaults to `StaticPairList`.*
***Datatype:*** *List of Dicts* | `telegram.enabled` | Enable the usage of Telegram.
***Datatype:*** *Boolean* -| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* -| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* | `webhook.enabled` | Enable usage of Webhook notifications
***Datatype:*** *Boolean* | `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
***Datatype:*** *String* -| `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
***Datatype:*** *String* -| `webhook.webhooksell` | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
***Datatype:*** *String* -| `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
***Datatype:*** *String* +| `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
***Datatype:*** *String* +| `webhook.webhooksell` | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
***Datatype:*** *String* +| `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
***Datatype:*** *String* | `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details.
***Datatype:*** *Boolean* | `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details.
***Datatype:*** *IPv4* -| `api_server.listen_port` | Bind Port. See the [API Server documentation](rest-api.md) for more details.
***Datatype:*** *Integer between 1024 and 65535* -| `api_server.username` | Username for API server. See the [API Server documentation](rest-api.md) for more details. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* -| `api_server.password` | Password for API server. See the [API Server documentation](rest-api.md) for more details. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* -| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances.
***Datatype:*** *String, SQLAlchemy connect string* +| `api_server.listen_port` | Bind Port. See the [API Server documentation](rest-api.md) for more details.
***Datatype:*** *Integer between 1024 and 65535* +| `api_server.username` | Username for API server. See the [API Server documentation](rest-api.md) for more details.
**Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `api_server.password` | Password for API server. See the [API Server documentation](rest-api.md) for more details.
**Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite:///tradesv3.dryrun.sqlite` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances.
***Datatype:*** *String, SQLAlchemy connect string* | `initial_state` | Defines the initial application state. More information below.
*Defaults to `stopped`.*
***Datatype:*** *Enum, either `stopped` or `running`* | `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below.
***Datatype:*** *Boolean* | `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`.
***Datatype:*** *ClassName* @@ -124,6 +124,7 @@ Values set in the configuration file always overwrite values set in the strategy * `order_time_in_force` * `stake_currency` * `stake_amount` +* `unfilledtimeout` * `use_sell_signal` (ask_strategy) * `sell_profit_only` (ask_strategy) * `ignore_roi_if_buy_signal` (ask_strategy) @@ -207,13 +208,6 @@ before asking the strategy if we should buy or a sell an asset. After each wait every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or the static list of pairs) if we should buy. -### Understand ask_last_balance - -The `ask_last_balance` configuration parameter sets the bidding price. Value `0.0` will use `ask` price, `1.0` will -use the `last` price and values between those interpolate between ask and last -price. Using `ask` price will guarantee quick success in bid, but bot will also -end up paying more then would probably have been necessary. - ### Understand order_types The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. @@ -393,6 +387,54 @@ The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT" ``` +## Prices used for orders + +Prices for regular orders can be controlled via the parameter structures `bid_strategy` for buying and `ask_strategy` for selling. +Prices are always retrieved right before an order is placed, either by querying the exchange tickers or by using the orderbook data. + +!!! Note + Orderbook data used by Freqtrade are the data retrieved from exchange by the ccxt's function `fetch_order_book()`, i.e. are usually data from the L2-aggregated orderbook, while the ticker data are the structures returned by the ccxt's `fetch_ticker()`/`fetch_tickers()` functions. Refer to the ccxt library [documentation](https://github.com/ccxt/ccxt/wiki/Manual#market-data) for more details. + +### Buy price + +#### Check depth of market + +When check depth of market is enabled (`bid_strategy.check_depth_of_market.enabled=True`), the buy signals are filtered based on the orderbook depth (sum of all amounts) for each orderbook side. + +Orderbook `bid` (buy) side depth is then divided by the orderbook `ask` (sell) side depth and the resulting delta is compared to the value of the `bid_strategy.check_depth_of_market.bids_to_ask_delta` parameter. The buy order is only executed if the orderbook delta is greater than or equal to the configured delta value. + +!!! Note + A delta value below 1 means that `ask` (sell) orderbook side depth is greater than the depth of the `bid` (buy) orderbook side, while a value greater than 1 means opposite (depth of the buy side is higher than the depth of the sell side). + +#### Buy price with Orderbook enabled + +When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Freqtrade fetches the `bid_strategy.order_book_top` entries from the orderbook and then uses the entry specified as `bid_strategy.order_book_top` on the `bid` (buy) side of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on. + +#### Buy price without Orderbook enabled + +When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `ask` (sell) price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `ask` price is not below the `last` price), it calculates a rate between `ask` and `last` price. + +The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `ask` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price. + +Using `ask` price often guarantees quicker success in the bid, but the bot can also end up paying more than what would have been necessary. + +### Sell price + +#### Sell price with Orderbook enabled + +When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Freqtrade fetches the `ask_strategy.order_book_max` entries in the orderbook. Then each of the orderbook steps between `ask_strategy.order_book_min` and `ask_strategy.order_book_max` on the `ask` orderbook side are validated for a profitable sell-possibility based on the strategy configuration and the sell order is placed at the first profitable spot. + +The idea here is to place the sell order early, to be ahead in the queue. + +A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting `ask_strategy.order_book_min` and `ask_strategy.order_book_max` to the same number. + +!!! Warning "Orderbook and stoploss_on_exchange" + Using `ask_strategy.order_book_max` higher than 1 may increase the risk, since an eventual [stoploss on exchange](#understand-order_types) will be needed to be cancelled as soon as the order is placed. + +#### Sell price without Orderbook enabled + +When not using orderbook (`ask_strategy.use_order_book=False`), the `bid` price from the ticker will be used as the sell price. + ## Pairlists Pairlists define the list of pairs that the bot should trade. diff --git a/docs/developer.md b/docs/developer.md index 5b07aff03..c679b8a49 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -266,4 +266,29 @@ Once the PR against master is merged (best right after merging): * Use the button "Draft a new release" in the Github UI (subsection releases). * Use the version-number specified as tag. * Use "master" as reference (this step comes after the above PR is merged). -* Use the above changelog as release comment (as codeblock). +* Use the above changelog as release comment (as codeblock) + +### After-release + +* Update version in develop by postfixing that with `-dev` (`2019.6 -> 2019.6-dev`). +* Create a PR against develop to update that branch. + +## Releases + +### pypi + +To create a pypi release, please run the following commands: + +Additional requirement: `wheel`, `twine` (for uploading), account on pypi with proper permissions. + +``` bash +python setup.py sdist bdist_wheel + +# For pypi test (to check if some change to the installation did work) +twine upload --repository-url https://test.pypi.org/legacy/ dist/* + +# For production: +twine upload dist/* +``` + +Please don't push non-releases to the productive / real pypi instance. diff --git a/docs/docker.md b/docs/docker.md index ff5bf7f25..d1684abc5 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -164,8 +164,7 @@ docker run -d \ ``` !!! Note - db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used. - To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite` + When using docker, it's best to specify `--db-url` explicitly to ensure that the database URL and the mounted database file match. !!! Note All available bot command line parameters can be added to the end of the `docker run` command. diff --git a/docs/edge.md b/docs/edge.md index c7b088476..e7909594e 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -1,4 +1,4 @@ -# Edge positioning +# Edge positioning This page explains how to use Edge Positioning module in your bot in order to enter into a trade only if the trade has a reasonable win rate and risk reward ratio, and consequently adjust your position size and stoploss. @@ -9,6 +9,7 @@ This page explains how to use Edge Positioning module in your bot in order to en Edge does not consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else are ignored in its calculation. ## Introduction + Trading is all about probability. No one can claim that he has a strategy working all the time. You have to assume that sometimes you lose. But it doesn't mean there is no rule, it only means rules should work "most of the time". Let's play a game: we toss a coin, heads: I give you 10$, tails: you give me 10$. Is it an interesting game? No, it's quite boring, isn't it? @@ -22,43 +23,61 @@ Let's complicate it more: you win 80% of the time but only 2$, I win 20% of the The question is: How do you calculate that? How do you know if you wanna play? The answer comes to two factors: + - Win Rate - Risk Reward Ratio ### Win Rate + Win Rate (*W*) is is the mean over some amount of trades (*N*) what is the percentage of winning trades to total number of trades (note that we don't consider how much you gained but only if you won or not). - W = (Number of winning trades) / (Total number of trades) = (Number of winning trades) / N +``` +W = (Number of winning trades) / (Total number of trades) = (Number of winning trades) / N +``` Complementary Loss Rate (*L*) is defined as - L = (Number of losing trades) / (Total number of trades) = (Number of losing trades) / N +``` +L = (Number of losing trades) / (Total number of trades) = (Number of losing trades) / N +``` or, which is the same, as - L = 1 – W +``` +L = 1 – W +``` ### Risk Reward Ratio + Risk Reward Ratio (*R*) is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose: - R = Profit / Loss +``` +R = Profit / Loss +``` Over time, on many trades, you can calculate your risk reward by dividing your average profit on winning trades by your average loss on losing trades: - Average profit = (Sum of profits) / (Number of winning trades) +``` +Average profit = (Sum of profits) / (Number of winning trades) - Average loss = (Sum of losses) / (Number of losing trades) +Average loss = (Sum of losses) / (Number of losing trades) - R = (Average profit) / (Average loss) +R = (Average profit) / (Average loss) +``` ### Expectancy + At this point we can combine *W* and *R* to create an expectancy ratio. This is a simple process of multiplying the risk reward ratio by the percentage of winning trades and subtracting the percentage of losing trades, which is calculated as follows: - Expectancy Ratio = (Risk Reward Ratio X Win Rate) – Loss Rate = (R X W) – L +``` +Expectancy Ratio = (Risk Reward Ratio X Win Rate) – Loss Rate = (R X W) – L +``` So lets say your Win rate is 28% and your Risk Reward Ratio is 5: - Expectancy = (5 X 0.28) – 0.72 = 0.68 +``` +Expectancy = (5 X 0.28) – 0.72 = 0.68 +``` Superficially, this means that on average you expect this strategy’s trades to return .68 times the size of your loses. This is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ. @@ -69,6 +88,7 @@ You can also use this value to evaluate the effectiveness of modifications to th **NOTICE:** It's important to keep in mind that Edge is testing your expectancy using historical data, there's no guarantee that you will have a similar edge in the future. It's still vital to do this testing in order to build confidence in your methodology, but be wary of "curve-fitting" your approach to the historical data as things are unlikely to play out the exact same way for future trades. ## How does it work? + If enabled in config, Edge will go through historical data with a range of stoplosses in order to find buy and sell/stoploss signals. It then calculates win rate and expectancy over *N* trades for each stoploss. Here is an example: | Pair | Stoploss | Win Rate | Risk Reward Ratio | Expectancy | @@ -83,6 +103,7 @@ The goal here is to find the best stoploss for the strategy in order to have the Edge module then forces stoploss value it evaluated to your strategy dynamically. ### Position size + Edge also dictates the stake amount for each trade to the bot according to the following factors: - Allowed capital at risk @@ -90,13 +111,17 @@ Edge also dictates the stake amount for each trade to the bot according to the f Allowed capital at risk is calculated as follows: - Allowed capital at risk = (Capital available_percentage) X (Allowed risk per trade) +``` +Allowed capital at risk = (Capital available_percentage) X (Allowed risk per trade) +``` Stoploss is calculated as described above against historical data. Your position size then will be: - Position size = (Allowed capital at risk) / Stoploss +``` +Position size = (Allowed capital at risk) / Stoploss +``` Example: @@ -115,100 +140,30 @@ Available capital doesn’t change before a position is sold. Let’s assume tha So the Bot receives another buy signal for trade 4 with a stoploss at 2% then your position size would be **0.055 / 0.02 = 2.75 ETH**. ## Configurations + Edge module has following configuration options: -#### enabled -If true, then Edge will run periodically. - -(defaults to false) - -#### process_throttle_secs -How often should Edge run in seconds? - -(defaults to 3600 so one hour) - -#### calculate_since_number_of_days -Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy -Note that it downloads historical data so increasing this number would lead to slowing down the bot. - -(defaults to 7) - -#### capital_available_percentage -This is the percentage of the total capital on exchange in stake currency. - -As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital. - -(defaults to 0.5) - -#### allowed_risk -Percentage of allowed risk per trade. - -(defaults to 0.01 so 1%) - -#### stoploss_range_min - -Minimum stoploss. - -(defaults to -0.01) - -#### stoploss_range_max - -Maximum stoploss. - -(defaults to -0.10) - -#### stoploss_range_step - -As an example if this is set to -0.01 then Edge will test the strategy for \[-0.01, -0,02, -0,03 ..., -0.09, -0.10\] ranges. -Note than having a smaller step means having a bigger range which could lead to slow calculation. - -If you set this parameter to -0.001, you then slow down the Edge calculation by a factor of 10. - -(defaults to -0.01) - -#### minimum_winrate - -It filters out pairs which don't have at least minimum_winrate. - -This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio. - -(defaults to 0.60) - -#### minimum_expectancy - -It filters out pairs which have the expectancy lower than this number. - -Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return. - -(defaults to 0.20) - -#### min_trade_number - -When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable. - -Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something. - -(defaults to 10, it is highly recommended not to decrease this number) - -#### max_trade_duration_minute - -Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign. - -**NOTICE:** While configuring this value, you should take into consideration your ticker interval. As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.). - -(defaults to 1 day, i.e. to 60 * 24 = 1440 minutes) - -#### remove_pumps - -Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off. - -(defaults to false) +| Parameter | Description | +|------------|-------------| +| `enabled` | If true, then Edge will run periodically.
*Defaults to `false`.*
***Datatype:*** *Boolean* +| `process_throttle_secs` | How often should Edge run in seconds.
*Defaults to `3600` (once per hour).*
***Datatype:*** *Integer* +| `calculate_since_number_of_days` | Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy.
**Note** that it downloads historical data so increasing this number would lead to slowing down the bot.
*Defaults to `7`.*
***Datatype:*** *Integer* +| `capital_available_percentage` | This is the percentage of the total capital on exchange in stake currency.
As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital.
*Defaults to `0.5`.*
***Datatype:*** *Float* +| `allowed_risk` | Ratio of allowed risk per trade.
*Defaults to `0.01` (1%)).*
***Datatype:*** *Float* +| `stoploss_range_min` | Minimum stoploss.
*Defaults to `-0.01`.*
***Datatype:*** *Float* +| `stoploss_range_max` | Maximum stoploss.
*Defaults to `-0.10`.*
***Datatype:*** *Float* +| `stoploss_range_step` | As an example if this is set to -0.01 then Edge will test the strategy for `[-0.01, -0,02, -0,03 ..., -0.09, -0.10]` ranges.
**Note** than having a smaller step means having a bigger range which could lead to slow calculation.
If you set this parameter to -0.001, you then slow down the Edge calculation by a factor of 10.
*Defaults to `-0.001`.*
***Datatype:*** *Float* +| `minimum_winrate` | It filters out pairs which don't have at least minimum_winrate.
This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio.
*Defaults to `0.60`.*
***Datatype:*** *Float* +| `minimum_expectancy` | It filters out pairs which have the expectancy lower than this number.
Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return.
*Defaults to `0.20`.*
***Datatype:*** *Float* +| `min_trade_number` | When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable.
Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something.
*Defaults to `10` (it is highly recommended not to decrease this number).*
***Datatype:*** *Integer* +| `max_trade_duration_minute` | Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.
**NOTICE:** While configuring this value, you should take into consideration your ticker interval. As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.).
*Defaults to `1440` (one day).*
***Datatype:*** *Integer* +| `remove_pumps` | Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off.
*Defaults to `false`.*
***Datatype:*** *Boolean* ## Running Edge independently You can run Edge independently in order to see in details the result. Here is an example: -```bash +``` bash freqtrade edge ``` diff --git a/docs/plotting.md b/docs/plotting.md index 982a5cd65..ba737562f 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -23,58 +23,43 @@ The `freqtrade plot-dataframe` subcommand shows an interactive graph with three Possible arguments: ``` -usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH] - [-d PATH] [--userdir PATH] [-s NAME] - [--strategy-path PATH] [-p PAIRS [PAIRS ...]] - [--indicators1 INDICATORS1 [INDICATORS1 ...]] - [--indicators2 INDICATORS2 [INDICATORS2 ...]] - [--plot-limit INT] [--db-url PATH] - [--trade-source {DB,file}] [--export EXPORT] - [--export-filename PATH] - [--timerange TIMERANGE] [-i TICKER_INTERVAL] +usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] + [--strategy-path PATH] [-p PAIRS [PAIRS ...]] [--indicators1 INDICATORS1 [INDICATORS1 ...]] + [--indicators2 INDICATORS2 [INDICATORS2 ...]] [--plot-limit INT] [--db-url PATH] + [--trade-source {DB,file}] [--export EXPORT] [--export-filename PATH] [--timerange TIMERANGE] + [-i TICKER_INTERVAL] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] - Show profits for only these pairs. Pairs are space- - separated. + Show profits for only these pairs. Pairs are space-separated. --indicators1 INDICATORS1 [INDICATORS1 ...] - Set indicators from your strategy you want in the - first row of the graph. Space-separated list. Example: + Set indicators from your strategy you want in the first row of the graph. Space-separated list. Example: `ema3 ema5`. Default: `['sma', 'ema3', 'ema5']`. --indicators2 INDICATORS2 [INDICATORS2 ...] - Set indicators from your strategy you want in the - third row of the graph. Space-separated list. Example: + Set indicators from your strategy you want in the third row of the graph. Space-separated list. Example: `fastd fastk`. Default: `['macd', 'macdsignal']`. - --plot-limit INT Specify tick limit for plotting. Notice: too high - values cause huge files. Default: 750. - --db-url PATH Override trades database URL, this is useful in custom - deployments (default: `sqlite:///tradesv3.sqlite` for - Live Run mode, `sqlite://` for Dry Run). + --plot-limit INT Specify tick limit for plotting. Notice: too high values cause huge files. Default: 750. + --db-url PATH Override trades database URL, this is useful in custom deployments (default: `sqlite:///tradesv3.sqlite` + for Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for Dry Run). --trade-source {DB,file} - Specify the source for trades (Can be DB or file - (backtest file)) Default: file - --export EXPORT Export backtest results, argument are: trades. - Example: `--export=trades` + Specify the source for trades (Can be DB or file (backtest file)) Default: file + --export EXPORT Export backtest results, argument are: trades. Example: `--export=trades` --export-filename PATH - Save backtest results to the file with this filename - (default: `user_data/backtest_results/backtest- - result.json`). Requires `--export` to be set as well. - Example: `--export-filename=user_data/backtest_results - /backtest_today.json` + Save backtest results to the file with this filename. Requires `--export` to be set as well. Example: + `--export-filename=user_data/backtest_results/backtest_today.json` --timerange TIMERANGE Specify what timerange of data to use. -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). + Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`). Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. + --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: `config.json`). - Multiple --config options may be used. Can be set to + Specify configuration file (default: `config.json`). 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. @@ -83,8 +68,7 @@ Common arguments: Strategy arguments: -s NAME, --strategy NAME - Specify strategy class name (default: - `DefaultStrategy`). + Specify strategy class name which will be used by the bot. --strategy-path PATH Specify additional strategy lookup path. ``` @@ -173,14 +157,14 @@ optional arguments: --export EXPORT Export backtest results, argument are: trades. Example: `--export=trades` --export-filename PATH - Save backtest results to the file with this filename - (default: `user_data/backtest_results/backtest- - result.json`). Requires `--export` to be set as well. - Example: `--export-filename=user_data/backtest_results - /backtest_today.json` + Save backtest results to the file with this filename. + Requires `--export` to be set as well. Example: + `--export-filename=user_data/backtest_results/backtest + _today.json` --db-url PATH Override trades database URL, this is useful in custom deployments (default: `sqlite:///tradesv3.sqlite` for - Live Run mode, `sqlite://` for Dry Run). + Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for + Dry Run). --trade-source {DB,file} Specify the source for trades (Can be DB or file (backtest file)) Default: file @@ -190,7 +174,9 @@ optional arguments: Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. + --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: `config.json`). diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 4efca7d2f..d59b097d7 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -455,6 +455,51 @@ Sample return value: ETH/BTC had 5 trades, with a total profit of 1.5% (ratio of !!! Warning Trade history is not available during backtesting or hyperopt. +### Prevent trades from happening for a specific pair + +Freqtrade locks pairs automatically for the current candle (until that candle is over) when a pair is sold, preventing an immediate re-buy of that pair. + +Locked pairs will show the message `Pair is currently locked.`. + +#### Locking pairs from within the strategy + +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. + +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_conf` will reset locked pairs. + +!!! Warning + Locking pairs is not functioning during backtesting. + +##### Pair locking example + +``` python +from freqtrade.persistence import Trade +from datetime import timedelta, datetime, timezone +# Put the above lines a the top of the strategy file, next to all the other imports +# -------- + +# Within populate indicators (or populate_buy): +if self.config['runmode'] in ('live', 'dry_run'): + # fetch closed trades for the last 2 days + trades = Trade.get_trades([Trade.pair == metadata['pair'], + Trade.open_date > datetime.utcnow() - timedelta(days=2), + Trade.is_open == False, + ]).all() + # Analyze the conditions you'd like to lock the pair .... will probably be different for every strategy + sumprofit = sum(trade.close_profit for trade in trades) + if sumprofit < 0: + # Lock pair for 12 hours + self.lock_pair(metadata['pair'], until=datetime.now(timezone.utc) + timedelta(hours=12)) +``` + ### Print created dataframe To inspect the created dataframe, you can issue a print-statement in either `populate_buy_trend()` or `populate_sell_trend()`. @@ -479,11 +524,6 @@ def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: Printing more than a few rows is also possible (simply use `print(dataframe)` instead of `print(dataframe.tail())`), however not recommended, as that will be very verbose (~500 lines per pair every 5 seconds). -### Where can i find a strategy template? - -The strategy template is located in the file -[user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py). - ### Specify custom strategy location If you want to use a strategy from a different directory you can pass `--strategy-path` diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index 9e61bda65..cc6b9805f 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -44,9 +44,9 @@ candles.head() ```python # Load strategy using values set above from freqtrade.resolvers import StrategyResolver -strategy = StrategyResolver({'strategy': strategy_name, - 'user_data_dir': user_data_dir, - 'strategy_path': strategy_location}).strategy +strategy = StrategyResolver.load_strategy({'strategy': strategy_name, + 'user_data_dir': user_data_dir, + 'strategy_path': strategy_location}) # Generate buy/sell signals using strategy df = strategy.analyze_ticker(candles, {'pair': pair}) diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 30902dfe9..4b6429f20 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -118,14 +118,14 @@ AVAILABLE_CLI_OPTIONS = { help='Specify what timerange of data to use.', ), "max_open_trades": Arg( - '--max_open_trades', - help='Specify max_open_trades to use.', + '--max-open-trades', + help='Override the value of the `max_open_trades` configuration setting.', type=int, metavar='INT', ), "stake_amount": Arg( - '--stake_amount', - help='Specify stake_amount.', + '--stake-amount', + help='Override the value of the `stake_amount` configuration setting.', type=float, ), # Backtesting diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index e517a0558..f73b52c10 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -223,13 +223,13 @@ class Configuration: logger.info('max_open_trades set to unlimited ...') elif 'max_open_trades' in self.args and self.args["max_open_trades"]: config.update({'max_open_trades': self.args["max_open_trades"]}) - logger.info('Parameter --max_open_trades detected, ' + logger.info('Parameter --max-open-trades detected, ' 'overriding max_open_trades to: %s ...', config.get('max_open_trades')) elif config['runmode'] in NON_UTIL_MODES: logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) self._args_to_config(config, argname='stake_amount', - logstring='Parameter --stake_amount detected, ' + logstring='Parameter --stake-amount detected, ' 'overriding stake_amount to: {} ...') self._args_to_config(config, argname='fee', @@ -403,7 +403,7 @@ class Configuration: config['pairs'] = config.get('exchange', {}).get('pair_whitelist') else: # Fall back to /dl_path/pairs.json - pairs_file = Path(config['datadir']) / "pairs.json" + pairs_file = config['datadir'] / "pairs.json" if pairs_file.exists(): with pairs_file.open('r') as f: config['pairs'] = json_load(f) diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index 3dd76a025..556f27742 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -9,7 +9,7 @@ from freqtrade.constants import USER_DATA_FILES logger = logging.getLogger(__name__) -def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str: +def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> Path: folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data") if not datadir: @@ -20,7 +20,7 @@ def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str if not folder.is_dir(): folder.mkdir(parents=True) logger.info(f'Created data directory: {datadir}') - return str(folder) + return folder def create_userdata_dir(directory: str, create_dir=False) -> Path: diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 5c7190b41..d7c6249d5 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -10,7 +10,7 @@ HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' -DEFAULT_DB_DRYRUN_URL = 'sqlite://' +DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite' UNLIMITED_STAKE_AMOUNT = 'unlimited' DEFAULT_AMOUNT_RESERVE_PERCENT = 0.05 REQUIRED_ORDERTIF = ['buy', 'sell'] diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 7b7159145..2964d1cb7 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -5,7 +5,6 @@ including Klines, tickers, historic data Common Interface for bot and strategy to access data. """ import logging -from pathlib import Path from typing import Any, Dict, List, Optional, Tuple from pandas import DataFrame @@ -65,7 +64,7 @@ class DataProvider: """ return load_pair_history(pair=pair, timeframe=timeframe or self._config['ticker_interval'], - datadir=Path(self._config['datadir']) + datadir=self._config['datadir'] ) def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame: diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index e56071a98..9ad2485ef 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -1,7 +1,6 @@ # pragma pylint: disable=W0603 """ Edge positioning package """ import logging -from pathlib import Path from typing import Any, Dict, NamedTuple import arrow @@ -96,7 +95,7 @@ class Edge: if self._refresh_pairs: history.refresh_data( - datadir=Path(self.config['datadir']), + datadir=self.config['datadir'], pairs=pairs, exchange=self.exchange, timeframe=self.strategy.ticker_interval, @@ -104,7 +103,7 @@ class Edge: ) data = history.load_data( - datadir=Path(self.config['datadir']), + datadir=self.config['datadir'], pairs=pairs, timeframe=self.strategy.ticker_interval, timerange=self._timerange, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e4e7aacce..01e84c06e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -278,7 +278,15 @@ class Exchange: raise OperationalException( f'Pair {pair} is not available on {self.name}. ' f'Please remove {pair} from your whitelist.') - elif self.markets[pair].get('info', {}).get('IsRestricted', False): + + # From ccxt Documentation: + # markets.info: An associative array of non-common market properties, + # including fees, rates, limits and other general market information. + # The internal info array is different for each particular market, + # its contents depend on the exchange. + # It can also be a string or similar ... so we need to verify that first. + elif (isinstance(self.markets[pair].get('info', None), dict) + and self.markets[pair].get('info', {}).get('IsRestricted', False)): # Warn users about restricted pairs in whitelist. # We cannot determine reliably if Users are affected. logger.warning(f"Pair {pair} is restricted for some users on this exchange." @@ -524,7 +532,7 @@ class Exchange: raise OperationalException(e) from e @retrier - def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: + def fetch_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: if refresh or pair not in self._cached_ticker.keys(): try: if pair not in self._api.markets or not self._api.markets[pair].get('active'): diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a51ab0dbf..ec41d910c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -55,12 +55,12 @@ class FreqtradeBot: self.heartbeat_interval = self.config.get('internals', {}).get('heartbeat_interval', 60) - self.strategy: IStrategy = StrategyResolver(self.config).strategy + self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) # Check config consistency here since strategies can set certain options validate_config_consistency(config) - self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange + self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) persistence.init(self.config.get('db_url', None), clean_open_orders=self.config.get('dry_run', False)) @@ -192,7 +192,7 @@ class FreqtradeBot: else: if not tick: logger.info('Using Last Ask / Last Price') - ticker = self.exchange.get_ticker(pair) + ticker = self.exchange.fetch_ticker(pair) else: ticker = tick if ticker['ask'] < ticker['last']: @@ -570,7 +570,7 @@ class FreqtradeBot: """ Get sell rate - either using get-ticker bid or first bid based on orderbook The orderbook portion is only used for rpc messaging, which would otherwise fail - for BitMex (has no bid/ask in get_ticker) + for BitMex (has no bid/ask in fetch_ticker) or remain static in any other case since it's not updating. :return: Bid rate """ @@ -582,7 +582,7 @@ class FreqtradeBot: rate = order_book['bids'][0][0] else: - rate = self.exchange.get_ticker(pair, refresh)['bid'] + rate = self.exchange.fetch_ticker(pair, refresh)['bid'] return rate def handle_trade(self, trade: Trade) -> bool: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 726257cdd..a8fe90a06 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -60,7 +60,7 @@ class Backtesting: # Reset keys for backtesting remove_credentials(self.config) self.strategylist: List[IStrategy] = [] - self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange + self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) if config.get('fee'): self.fee = config['fee'] @@ -75,12 +75,12 @@ class Backtesting: for strat in list(self.config['strategy_list']): stratconf = deepcopy(self.config) stratconf['strategy'] = strat - self.strategylist.append(StrategyResolver(stratconf).strategy) + self.strategylist.append(StrategyResolver.load_strategy(stratconf)) validate_config_consistency(stratconf) else: # No strategy list specified, only one strategy - self.strategylist.append(StrategyResolver(self.config).strategy) + self.strategylist.append(StrategyResolver.load_strategy(self.config)) validate_config_consistency(self.config) if "ticker_interval" not in self.config: @@ -109,7 +109,7 @@ class Backtesting: 'timerange') is None else str(self.config.get('timerange'))) data = history.load_data( - datadir=Path(self.config['datadir']), + datadir=self.config['datadir'], pairs=self.config['exchange']['pair_whitelist'], timeframe=self.timeframe, timerange=timerange, @@ -183,9 +183,11 @@ class Backtesting: Generate small table outlining Backtest results """ tabular_data = [] - headers = ['Sell Reason', 'Count'] + headers = ['Sell Reason', 'Count', 'Profit', 'Loss'] for reason, count in results['sell_reason'].value_counts().iteritems(): - tabular_data.append([reason.value, count]) + profit = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] >= 0)]) + loss = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] < 0)]) + tabular_data.append([reason.value, count, profit, loss]) return tabulate(tabular_data, headers=headers, tablefmt="pipe") def _generate_text_table_strategy(self, all_results: dict) -> str: diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index a667ebb92..ea5cc663d 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -34,7 +34,7 @@ class EdgeCli: remove_credentials(self.config) self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT self.exchange = Exchange(self.config) - self.strategy = StrategyResolver(self.config).strategy + self.strategy = StrategyResolver.load_strategy(self.config) validate_config_consistency(self.config) @@ -42,11 +42,9 @@ class EdgeCli: # Set refresh_pairs to false for edge-cli (it must be true for edge) self.edge._refresh_pairs = False - self.timerange = TimeRange.parse_timerange(None if self.config.get( + self.edge._timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) - self.edge._timerange = self.timerange - def _generate_edge_table(self, results: dict) -> str: floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d') diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 521a4d790..48f883ac5 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -64,9 +64,9 @@ class Hyperopt: self.backtesting = Backtesting(self.config) - self.custom_hyperopt = HyperOptResolver(self.config).hyperopt + self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) - self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss + self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config) self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function self.trials_file = (self.config['user_data_dir'] / diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index fa5382c37..1530710d2 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -28,13 +28,13 @@ class PairListManager(): if 'method' not in pl: logger.warning(f"No method in {pl}") continue - pairl = PairListResolver(pl.get('method'), - exchange=exchange, - pairlistmanager=self, - config=config, - pairlistconfig=pl, - pairlist_pos=len(self._pairlists) - ).pairlist + pairl = PairListResolver.load_pairlist(pl.get('method'), + exchange=exchange, + pairlistmanager=self, + config=config, + pairlistconfig=pl, + pairlist_pos=len(self._pairlists) + ) self._tickers_needed = pairl.needstickers or self._tickers_needed self._pairlists.append(pairl) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 85089af9c..db4637ee5 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -37,7 +37,7 @@ def init_plotscript(config): timerange = TimeRange.parse_timerange(config.get("timerange")) tickers = history.load_data( - datadir=Path(str(config.get("datadir"))), + datadir=config.get("datadir"), pairs=pairs, timeframe=config.get('ticker_interval', '5m'), timerange=timerange, @@ -340,7 +340,7 @@ def load_and_plot_trades(config: Dict[str, Any]): - Generate plot files :return: None """ - strategy = StrategyResolver(config).strategy + strategy = StrategyResolver.load_strategy(config) plot_elements = init_plotscript(config) trades = plot_elements['trades'] diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 60f37b1c9..e28a5cf80 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -15,9 +15,8 @@ class ExchangeResolver(IResolver): This class contains all the logic to load a custom exchange class """ - __slots__ = ['exchange'] - - def __init__(self, exchange_name: str, config: dict, validate: bool = True) -> None: + @staticmethod + def load_exchange(exchange_name: str, config: dict, validate: bool = True) -> Exchange: """ Load the custom class from config parameter :param config: configuration dictionary @@ -25,17 +24,20 @@ class ExchangeResolver(IResolver): # Map exchange name to avoid duplicate classes for identical exchanges exchange_name = MAP_EXCHANGE_CHILDCLASS.get(exchange_name, exchange_name) exchange_name = exchange_name.title() + exchange = None try: - self.exchange = self._load_exchange(exchange_name, kwargs={'config': config, - 'validate': validate}) + exchange = ExchangeResolver._load_exchange(exchange_name, + kwargs={'config': config, + 'validate': validate}) except ImportError: logger.info( f"No {exchange_name} specific subclass found. Using the generic class instead.") - if not hasattr(self, "exchange"): - self.exchange = Exchange(config, validate=validate) + if not exchange: + exchange = Exchange(config, validate=validate) + return exchange - def _load_exchange( - self, exchange_name: str, kwargs: dict) -> Exchange: + @staticmethod + def _load_exchange(exchange_name: str, kwargs: dict) -> Exchange: """ Loads the specified exchange. Only checks for exchanges exported in freqtrade.exchanges diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 05efa1164..0726b0627 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -20,11 +20,11 @@ class HyperOptResolver(IResolver): """ This class contains all the logic to load custom hyperopt class """ - __slots__ = ['hyperopt'] - def __init__(self, config: Dict) -> None: + @staticmethod + def load_hyperopt(config: Dict) -> IHyperOpt: """ - Load the custom class from config parameter + Load the custom hyperopt class from config parameter :param config: configuration dictionary """ if not config.get('hyperopt'): @@ -33,21 +33,23 @@ class HyperOptResolver(IResolver): hyperopt_name = config['hyperopt'] - self.hyperopt = self._load_hyperopt(hyperopt_name, config, - extra_dir=config.get('hyperopt_path')) + hyperopt = HyperOptResolver._load_hyperopt(hyperopt_name, config, + extra_dir=config.get('hyperopt_path')) - if not hasattr(self.hyperopt, 'populate_indicators'): + if not hasattr(hyperopt, 'populate_indicators'): logger.warning("Hyperopt class does not provide populate_indicators() method. " "Using populate_indicators from the strategy.") - if not hasattr(self.hyperopt, 'populate_buy_trend'): + if not hasattr(hyperopt, 'populate_buy_trend'): logger.warning("Hyperopt class does not provide populate_buy_trend() method. " "Using populate_buy_trend from the strategy.") - if not hasattr(self.hyperopt, 'populate_sell_trend'): + if not hasattr(hyperopt, 'populate_sell_trend'): logger.warning("Hyperopt class does not provide populate_sell_trend() method. " "Using populate_sell_trend from the strategy.") + return hyperopt + @staticmethod def _load_hyperopt( - self, hyperopt_name: str, config: Dict, extra_dir: Optional[str] = None) -> IHyperOpt: + hyperopt_name: str, config: Dict, extra_dir: Optional[str] = None) -> IHyperOpt: """ Search and loads the specified hyperopt. :param hyperopt_name: name of the module to import @@ -57,11 +59,12 @@ class HyperOptResolver(IResolver): """ current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() - abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir=USERPATH_HYPEROPTS, extra_dir=extra_dir) + abs_paths = IResolver.build_search_paths(config, current_path=current_path, + user_subdir=USERPATH_HYPEROPTS, + extra_dir=extra_dir) - hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt, - object_name=hyperopt_name, kwargs={'config': config}) + hyperopt = IResolver._load_object(paths=abs_paths, object_type=IHyperOpt, + object_name=hyperopt_name, kwargs={'config': config}) if hyperopt: return hyperopt raise OperationalException( @@ -74,9 +77,9 @@ class HyperOptLossResolver(IResolver): """ This class contains all the logic to load custom hyperopt loss class """ - __slots__ = ['hyperoptloss'] - def __init__(self, config: Dict) -> None: + @staticmethod + def load_hyperoptloss(config: Dict) -> IHyperOptLoss: """ Load the custom class from config parameter :param config: configuration dictionary @@ -86,20 +89,21 @@ class HyperOptLossResolver(IResolver): # default hyperopt loss hyperoptloss_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS - self.hyperoptloss = self._load_hyperoptloss( + hyperoptloss = HyperOptLossResolver._load_hyperoptloss( hyperoptloss_name, config, extra_dir=config.get('hyperopt_path')) # Assign ticker_interval to be used in hyperopt - self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval']) + hyperoptloss.__class__.ticker_interval = str(config['ticker_interval']) - if not hasattr(self.hyperoptloss, 'hyperopt_loss_function'): + if not hasattr(hyperoptloss, 'hyperopt_loss_function'): raise OperationalException( f"Found HyperoptLoss class {hyperoptloss_name} does not " "implement `hyperopt_loss_function`.") + return hyperoptloss - def _load_hyperoptloss( - self, hyper_loss_name: str, config: Dict, - extra_dir: Optional[str] = None) -> IHyperOptLoss: + @staticmethod + def _load_hyperoptloss(hyper_loss_name: str, config: Dict, + extra_dir: Optional[str] = None) -> IHyperOptLoss: """ Search and loads the specified hyperopt loss class. :param hyper_loss_name: name of the module to import @@ -109,11 +113,12 @@ class HyperOptLossResolver(IResolver): """ current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() - abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir=USERPATH_HYPEROPTS, extra_dir=extra_dir) + abs_paths = IResolver.build_search_paths(config, current_path=current_path, + user_subdir=USERPATH_HYPEROPTS, + extra_dir=extra_dir) - hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss, - object_name=hyper_loss_name) + hyperoptloss = IResolver._load_object(paths=abs_paths, object_type=IHyperOptLoss, + object_name=hyper_loss_name) if hyperoptloss: return hyperoptloss diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 3bad42fd9..0b986debb 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -17,7 +17,8 @@ class IResolver: This class contains all the logic to load custom classes """ - def build_search_paths(self, config, current_path: Path, user_subdir: Optional[str] = None, + @staticmethod + def build_search_paths(config, current_path: Path, user_subdir: Optional[str] = None, extra_dir: Optional[str] = None) -> List[Path]: abs_paths: List[Path] = [current_path] diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index d849f4ffb..611660ff4 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -18,23 +18,29 @@ class PairListResolver(IResolver): This class contains all the logic to load custom PairList class """ - __slots__ = ['pairlist'] - - def __init__(self, pairlist_name: str, exchange, pairlistmanager, - config: dict, pairlistconfig: dict, pairlist_pos: int) -> None: + @staticmethod + def load_pairlist(pairlist_name: str, exchange, pairlistmanager, + config: dict, pairlistconfig: dict, pairlist_pos: int) -> IPairList: """ - Load the custom class from config parameter - :param config: configuration dictionary or None + Load the pairlist with pairlist_name + :param pairlist_name: Classname of the pairlist + :param exchange: Initialized exchange class + :param pairlistmanager: Initialized pairlist manager + :param config: configuration dictionary + :param pairlistconfig: Configuration dedicated to this pairlist + :param pairlist_pos: Position of the pairlist in the list of pairlists + :return: initialized Pairlist class """ - self.pairlist = self._load_pairlist(pairlist_name, config, - kwargs={'exchange': exchange, - 'pairlistmanager': pairlistmanager, - 'config': config, - 'pairlistconfig': pairlistconfig, - 'pairlist_pos': pairlist_pos}) - def _load_pairlist( - self, pairlist_name: str, config: dict, kwargs: dict) -> IPairList: + return PairListResolver._load_pairlist(pairlist_name, config, + kwargs={'exchange': exchange, + 'pairlistmanager': pairlistmanager, + 'config': config, + 'pairlistconfig': pairlistconfig, + 'pairlist_pos': pairlist_pos}) + + @staticmethod + def _load_pairlist(pairlist_name: str, config: dict, kwargs: dict) -> IPairList: """ Search and loads the specified pairlist. :param pairlist_name: name of the module to import @@ -44,11 +50,11 @@ class PairListResolver(IResolver): """ current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() - abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir=None, extra_dir=None) + abs_paths = IResolver.build_search_paths(config, current_path=current_path, + user_subdir=None, extra_dir=None) - pairlist = self._load_object(paths=abs_paths, object_type=IPairList, - object_name=pairlist_name, kwargs=kwargs) + pairlist = IResolver._load_object(paths=abs_paths, object_type=IPairList, + object_name=pairlist_name, kwargs=kwargs) if pairlist: return pairlist raise OperationalException( diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 9a76b9b74..6d3fe5ff9 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -20,12 +20,11 @@ logger = logging.getLogger(__name__) class StrategyResolver(IResolver): """ - This class contains all the logic to load custom strategy class + This class contains the logic to load custom strategy class """ - __slots__ = ['strategy'] - - def __init__(self, config: Optional[Dict] = None) -> None: + @staticmethod + def load_strategy(config: Optional[Dict] = None) -> IStrategy: """ Load the custom class from config parameter :param config: configuration dictionary or None @@ -37,9 +36,9 @@ class StrategyResolver(IResolver): "the strategy class to use.") strategy_name = config['strategy'] - self.strategy: IStrategy = self._load_strategy(strategy_name, - config=config, - extra_dir=config.get('strategy_path')) + strategy: IStrategy = StrategyResolver._load_strategy( + strategy_name, config=config, + extra_dir=config.get('strategy_path')) # make sure ask_strategy dict is available if 'ask_strategy' not in config: @@ -61,15 +60,18 @@ class StrategyResolver(IResolver): ("stake_currency", None, False), ("stake_amount", None, False), ("startup_candle_count", None, False), + ("unfilledtimeout", None, False), ("use_sell_signal", True, True), ("sell_profit_only", False, True), ("ignore_roi_if_buy_signal", False, True), ] for attribute, default, ask_strategy in attributes: if ask_strategy: - self._override_attribute_helper(config['ask_strategy'], attribute, default) + StrategyResolver._override_attribute_helper(strategy, config['ask_strategy'], + attribute, default) else: - self._override_attribute_helper(config, attribute, default) + StrategyResolver._override_attribute_helper(strategy, config, + attribute, default) # Loop this list again to have output combined for attribute, _, exp in attributes: @@ -79,14 +81,16 @@ class StrategyResolver(IResolver): logger.info("Strategy using %s: %s", attribute, config[attribute]) # Sort and apply type conversions - self.strategy.minimal_roi = OrderedDict(sorted( - {int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(), + strategy.minimal_roi = OrderedDict(sorted( + {int(key): value for (key, value) in strategy.minimal_roi.items()}.items(), key=lambda t: t[0])) - self.strategy.stoploss = float(self.strategy.stoploss) + strategy.stoploss = float(strategy.stoploss) - self._strategy_sanity_validations() + StrategyResolver._strategy_sanity_validations(strategy) + return strategy - def _override_attribute_helper(self, config, attribute: str, default): + @staticmethod + def _override_attribute_helper(strategy, config, attribute: str, default): """ Override attributes in the strategy. Prevalence: @@ -95,30 +99,32 @@ class StrategyResolver(IResolver): - default (if not None) """ if attribute in config: - setattr(self.strategy, attribute, config[attribute]) + setattr(strategy, attribute, config[attribute]) logger.info("Override strategy '%s' with value in config file: %s.", attribute, config[attribute]) - elif hasattr(self.strategy, attribute): - val = getattr(self.strategy, attribute) + elif hasattr(strategy, attribute): + val = getattr(strategy, attribute) # None's cannot exist in the config, so do not copy them if val is not None: config[attribute] = val # Explicitly check for None here as other "falsy" values are possible elif default is not None: - setattr(self.strategy, attribute, default) + setattr(strategy, attribute, default) config[attribute] = default - def _strategy_sanity_validations(self): - if not all(k in self.strategy.order_types for k in constants.REQUIRED_ORDERTYPES): - raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. " + @staticmethod + def _strategy_sanity_validations(strategy): + if not all(k in strategy.order_types for k in constants.REQUIRED_ORDERTYPES): + raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. " f"Order-types mapping is incomplete.") - if not all(k in self.strategy.order_time_in_force for k in constants.REQUIRED_ORDERTIF): - raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. " + if not all(k in strategy.order_time_in_force for k in constants.REQUIRED_ORDERTIF): + raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. " f"Order-time-in-force mapping is incomplete.") - def _load_strategy( - self, strategy_name: str, config: dict, extra_dir: Optional[str] = None) -> IStrategy: + @staticmethod + def _load_strategy(strategy_name: str, + config: dict, extra_dir: Optional[str] = None) -> IStrategy: """ Search and loads the specified strategy. :param strategy_name: name of the module to import @@ -128,9 +134,9 @@ class StrategyResolver(IResolver): """ current_path = Path(__file__).parent.parent.joinpath('strategy').resolve() - abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir=constants.USERPATH_STRATEGY, - extra_dir=extra_dir) + abs_paths = IResolver.build_search_paths(config, current_path=current_path, + user_subdir=constants.USERPATH_STRATEGY, + extra_dir=extra_dir) if ":" in strategy_name: logger.info("loading base64 encoded strategy") @@ -148,8 +154,8 @@ class StrategyResolver(IResolver): # register temp path with the bot abs_paths.insert(0, temp.resolve()) - strategy = self._load_object(paths=abs_paths, object_type=IStrategy, - object_name=strategy_name, kwargs={'config': config}) + strategy = IResolver._load_object(paths=abs_paths, object_type=IStrategy, + object_name=strategy_name, 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/rpc.py b/freqtrade/rpc/rpc.py index 3b4b7570a..d6d442df5 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -341,13 +341,14 @@ class RPC: raise RPCException('All balances are zero.') symbol = fiat_display_currency - value = self._fiat_converter.convert_amount(total, 'BTC', + value = self._fiat_converter.convert_amount(total, stake_currency, symbol) if self._fiat_converter else 0 return { 'currencies': output, 'total': total, 'symbol': symbol, 'value': value, + 'stake': stake_currency, 'note': 'Simulated balances' if self._freqtrade.config.get('dry_run', False) else '' } diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e0e2afd7b..e9ecdcff6 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -335,7 +335,7 @@ class Telegram(RPC): output = '' if self._config['dry_run']: output += ( - f"*Warning:*Simulated balances in Dry Mode.\n" + f"*Warning:* Simulated balances in Dry Mode.\n" "This mode is still experimental!\n" "Starting capital: " f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" @@ -358,7 +358,7 @@ class Telegram(RPC): output += curr_output output += "\n*Estimated Value*:\n" \ - "\t`BTC: {total: .8f}`\n" \ + "\t`{stake}: {total: .8f}`\n" \ "\t`{symbol}: {value: .2f}`\n".format(**result) self._send_msg(output) except RPCException as e: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 985ff37de..4f2e990d2 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -168,11 +168,24 @@ class IStrategy(ABC): """ Locks pair until a given timestamp happens. Locked pairs are not analyzed, and are prevented from opening new trades. + Locks can only count up (allowing users to lock pairs for a longer period of time). + To remove a lock from a pair, use `unlock_pair()` :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)` """ - self._pair_locked_until[pair] = until + if pair not in self._pair_locked_until or self._pair_locked_until[pair] < until: + self._pair_locked_until[pair] = until + + def unlock_pair(self, pair) -> None: + """ + Unlocks a pair previously locked using lock_pair. + Not used by freqtrade itself, but intended to be used if users lock pairs + 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] def is_pair_locked(self, pair: str) -> bool: """ diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index 2876ea938..eea8fb85f 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -73,9 +73,9 @@ "source": [ "# Load strategy using values set above\n", "from freqtrade.resolvers import StrategyResolver\n", - "strategy = StrategyResolver({'strategy': strategy_name,\n", - " 'user_data_dir': user_data_dir,\n", - " 'strategy_path': strategy_location}).strategy\n", + "strategy = StrategyResolver.load_strategy({'strategy': strategy_name,\n", + " 'user_data_dir': user_data_dir,\n", + " 'strategy_path': strategy_location})\n", "\n", "# Generate buy/sell signals using strategy\n", "df = strategy.analyze_ticker(candles, {'pair': pair})\n", diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 9e01c7ea6..a638e5ff0 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -191,29 +191,28 @@ def start_download_data(args: Dict[str, Any]) -> None: "Downloading data requires a list of pairs. " "Please check the documentation on how to configure this.") - dl_path = Path(config['datadir']) logger.info(f'About to download pairs: {config["pairs"]}, ' - f'intervals: {config["timeframes"]} to {dl_path}') + f'intervals: {config["timeframes"]} to {config["datadir"]}') pairs_not_available: List[str] = [] # Init exchange - exchange = ExchangeResolver(config['exchange']['name'], config).exchange + exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) try: if config.get('download_trades'): pairs_not_available = refresh_backtest_trades_data( - exchange, pairs=config["pairs"], datadir=Path(config['datadir']), + exchange, pairs=config["pairs"], datadir=config['datadir'], timerange=timerange, erase=config.get("erase")) # Convert downloaded trade data to different timeframes convert_trades_to_ohlcv( pairs=config["pairs"], timeframes=config["timeframes"], - datadir=Path(config['datadir']), timerange=timerange, erase=config.get("erase")) + datadir=config['datadir'], timerange=timerange, erase=config.get("erase")) else: pairs_not_available = refresh_backtest_ohlcv_data( exchange, pairs=config["pairs"], timeframes=config["timeframes"], - datadir=Path(config['datadir']), timerange=timerange, erase=config.get("erase")) + datadir=config['datadir'], timerange=timerange, erase=config.get("erase")) except KeyboardInterrupt: sys.exit("SIGINT received, aborting ...") @@ -233,7 +232,7 @@ def start_list_timeframes(args: Dict[str, Any]) -> None: config['ticker_interval'] = None # Init exchange - exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange + exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) if args['print_one_column']: print('\n'.join(exchange.timeframes)) @@ -252,7 +251,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) # Init exchange - exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange + exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) # By default only active pairs/markets are to be shown active_only = not args.get('list_pairs_all', False) @@ -333,7 +332,7 @@ def start_test_pairlist(args: Dict[str, Any]) -> None: from freqtrade.pairlist.pairlistmanager import PairListManager config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) - exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange + exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) quote_currencies = args.get('quote_currencies') if not quote_currencies: diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index dd706438f..54c3f9138 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -58,13 +58,15 @@ class Wallets: - Subtract currently tied up stake_amount in open trades - update balances for currencies currently in trades """ + # Recreate _wallets to reset closed trade balances + _wallets = {} closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all() open_trades = Trade.get_trades(Trade.is_open.is_(True)).all() tot_profit = sum([trade.calc_profit() for trade in closed_trades]) tot_in_trades = sum([trade.stake_amount for trade in open_trades]) current_stake = self.start_cap + tot_profit - tot_in_trades - self._wallets[self._config['stake_currency']] = Wallet( + _wallets[self._config['stake_currency']] = Wallet( self._config['stake_currency'], current_stake, 0, @@ -73,12 +75,13 @@ class Wallets: for trade in open_trades: curr = trade.pair.split('/')[0] - self._wallets[curr] = Wallet( + _wallets[curr] = Wallet( curr, trade.amount, 0, trade.amount ) + self._wallets = _wallets def _update_live(self) -> None: diff --git a/requirements-common.txt b/requirements-common.txt index a6d9a6f5e..9d0fd4756 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,7 +1,7 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.20.84 -SQLAlchemy==1.3.11 +ccxt==1.21.12 +SQLAlchemy==1.3.12 python-telegram-bot==12.2.0 arrow==0.15.4 cachetools==4.0.0 diff --git a/requirements-dev.txt b/requirements-dev.txt index fe5b4e369..1357bba00 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ coveralls==1.9.2 flake8==3.7.9 flake8-type-annotations==0.1.0 flake8-tidy-imports==3.1.0 -mypy==0.750 +mypy==0.761 pytest==5.3.2 pytest-asyncio==0.10.0 pytest-cov==2.8.1 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index b2428e37d..9b408dbed 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.3.3 +scipy==1.4.1 scikit-learn==0.22 scikit-optimize==0.5.2 filelock==3.0.12 diff --git a/requirements.txt b/requirements.txt index ebf27abd4..e0e2942b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Load common requirements -r requirements-common.txt -numpy==1.17.4 +numpy==1.18.0 pandas==0.25.3 diff --git a/setup.py b/setup.py index 3710bcdc0..7d8d7b68d 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ setup(name='freqtrade', license='GPLv3', packages=['freqtrade'], setup_requires=['pytest-runner', 'numpy'], - tests_require=['pytest', 'pytest-mock', 'pytest-cov'], + tests_require=['pytest', 'pytest-asyncio', 'pytest-cov', 'pytest-mock', ], install_requires=[ # from requirements-common.txt 'ccxt>=1.18.1080', @@ -99,8 +99,12 @@ setup(name='freqtrade', ], }, classifiers=[ - 'Programming Language :: Python :: 3.6', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Topic :: Office/Business :: Financial :: Investment', + 'Environment :: Console', 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Operating System :: MacOS', + 'Operating System :: Unix', + 'Topic :: Office/Business :: Financial :: Investment', ]) diff --git a/tests/conftest.py b/tests/conftest.py index 82111528e..501f89fff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -77,7 +77,7 @@ def get_patched_exchange(mocker, config, api_mock=None, id='bittrex', patch_exchange(mocker, api_mock, id, mock_markets) config["exchange"]["name"] = id try: - exchange = ExchangeResolver(id, config).exchange + exchange = ExchangeResolver.load_exchange(id, config) except ImportError: exchange = Exchange(config) return exchange diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 629f99aa2..2f95e4e01 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -124,19 +124,19 @@ def test_exchange_resolver(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - exchange = ExchangeResolver('Bittrex', default_conf).exchange + exchange = ExchangeResolver.load_exchange('Bittrex', 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('kraken', default_conf).exchange + exchange = ExchangeResolver.load_exchange('kraken', default_conf) assert isinstance(exchange, Exchange) assert isinstance(exchange, Kraken) assert not isinstance(exchange, Binance) assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) - exchange = ExchangeResolver('binance', default_conf).exchange + exchange = ExchangeResolver.load_exchange('binance', default_conf) assert isinstance(exchange, Exchange) assert isinstance(exchange, Binance) assert not isinstance(exchange, Kraken) @@ -145,7 +145,7 @@ def test_exchange_resolver(default_conf, mocker, caplog): caplog) # Test mapping - exchange = ExchangeResolver('binanceus', default_conf).exchange + exchange = ExchangeResolver.load_exchange('binanceus', default_conf) assert isinstance(exchange, Exchange) assert isinstance(exchange, Binance) assert not isinstance(exchange, Kraken) @@ -363,8 +363,9 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): def test_validate_pairs_restricted(default_conf, mocker, caplog): api_mock = MagicMock() type(api_mock).markets = PropertyMock(return_value={ - 'ETH/BTC': {}, 'LTC/BTC': {}, 'NEO/BTC': {}, - 'XRP/BTC': {'info': {'IsRestricted': True}} + 'ETH/BTC': {}, 'LTC/BTC': {}, + 'XRP/BTC': {'info': {'IsRestricted': True}}, + 'NEO/BTC': {'info': 'TestString'}, # info can also be a string ... }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) @@ -977,7 +978,7 @@ def test_get_tickers(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_ticker(default_conf, mocker, exchange_name): +def test_fetch_ticker(default_conf, mocker, exchange_name): api_mock = MagicMock() tick = { 'symbol': 'ETH/BTC', @@ -989,7 +990,7 @@ def test_get_ticker(default_conf, mocker, exchange_name): api_mock.markets = {'ETH/BTC': {'active': True}} exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) # retrieve original ticker - ticker = exchange.get_ticker(pair='ETH/BTC') + ticker = exchange.fetch_ticker(pair='ETH/BTC') assert ticker['bid'] == 0.00001098 assert ticker['ask'] == 0.00001099 @@ -1006,7 +1007,7 @@ def test_get_ticker(default_conf, mocker, exchange_name): # if not caching the result we should get the same ticker # if not fetching a new result we should get the cached ticker - ticker = exchange.get_ticker(pair='ETH/BTC') + ticker = exchange.fetch_ticker(pair='ETH/BTC') assert api_mock.fetch_ticker.call_count == 1 assert ticker['bid'] == 0.5 @@ -1018,19 +1019,19 @@ def test_get_ticker(default_conf, mocker, exchange_name): # Test caching api_mock.fetch_ticker = MagicMock() - exchange.get_ticker(pair='ETH/BTC', refresh=False) + exchange.fetch_ticker(pair='ETH/BTC', refresh=False) assert api_mock.fetch_ticker.call_count == 0 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - "get_ticker", "fetch_ticker", + "fetch_ticker", "fetch_ticker", pair='ETH/BTC', refresh=True) api_mock.fetch_ticker = MagicMock(return_value={}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.get_ticker(pair='ETH/BTC', refresh=True) + exchange.fetch_ticker(pair='ETH/BTC', refresh=True) with pytest.raises(DependencyException, match=r'Pair XRP/ETH not available'): - exchange.get_ticker(pair='XRP/ETH', refresh=True) + exchange.fetch_ticker(pair='XRP/ETH', refresh=True) @pytest.mark.parametrize("exchange_name", EXCHANGES) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 0ea35e41f..8a27c591f 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -394,8 +394,8 @@ def test_generate_text_table_sell_reason(default_conf, mocker): results = pd.DataFrame( { 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], - 'profit_percent': [0.1, 0.2, 0.3], - 'profit_abs': [0.2, 0.4, 0.5], + 'profit_percent': [0.1, 0.2, -0.3], + 'profit_abs': [0.2, 0.4, -0.5], 'trade_duration': [10, 30, 10], 'profit': [2, 0, 0], 'loss': [0, 0, 1], @@ -404,10 +404,10 @@ def test_generate_text_table_sell_reason(default_conf, mocker): ) result_str = ( - '| Sell Reason | Count |\n' - '|:--------------|--------:|\n' - '| roi | 2 |\n' - '| stop_loss | 1 |' + '| Sell Reason | Count | Profit | Loss |\n' + '|:--------------|--------:|---------:|-------:|\n' + '| roi | 2 | 2 | 0 |\n' + '| stop_loss | 1 | 0 | 1 |' ) assert backtesting._generate_text_table_sell_reason( data={'ETH/BTC': {}}, results=results) == result_str diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 29b8b5b16..9c6e73c53 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -163,7 +163,7 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: MagicMock(return_value=hyperopt(default_conf)) ) default_conf.update({'hyperopt': 'DefaultHyperOpt'}) - x = HyperOptResolver(default_conf).hyperopt + x = HyperOptResolver.load_hyperopt(default_conf) assert not hasattr(x, 'populate_indicators') assert not hasattr(x, 'populate_buy_trend') assert not hasattr(x, 'populate_sell_trend') @@ -180,7 +180,7 @@ def test_hyperoptresolver_wrongname(mocker, default_conf, caplog) -> None: default_conf.update({'hyperopt': "NonExistingHyperoptClass"}) with pytest.raises(OperationalException, match=r'Impossible to load Hyperopt.*'): - HyperOptResolver(default_conf).hyperopt + HyperOptResolver.load_hyperopt(default_conf) def test_hyperoptresolver_noname(default_conf): @@ -188,7 +188,7 @@ def test_hyperoptresolver_noname(default_conf): with pytest.raises(OperationalException, match="No Hyperopt set. Please use `--hyperopt` to specify " "the Hyperopt class to use."): - HyperOptResolver(default_conf) + HyperOptResolver.load_hyperopt(default_conf) def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None: @@ -198,7 +198,7 @@ def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None: 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss', MagicMock(return_value=hl) ) - x = HyperOptLossResolver(default_conf).hyperoptloss + x = HyperOptLossResolver.load_hyperoptloss(default_conf) assert hasattr(x, "hyperopt_loss_function") @@ -206,7 +206,7 @@ def test_hyperoptlossresolver_wrongname(mocker, default_conf, caplog) -> None: default_conf.update({'hyperopt_loss': "NonExistingLossClass"}) with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'): - HyperOptLossResolver(default_conf).hyperopt + HyperOptLossResolver.load_hyperoptloss(default_conf) def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None: @@ -286,7 +286,7 @@ def test_start_filelock(mocker, default_conf, caplog) -> None: def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_results) -> None: - hl = HyperOptLossResolver(default_conf).hyperoptloss + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) correct = hl.hyperopt_loss_function(hyperopt_results, 600) over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100) under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100) @@ -298,7 +298,7 @@ def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results) resultsb = hyperopt_results.copy() resultsb.loc[1, 'trade_duration'] = 20 - hl = HyperOptLossResolver(default_conf).hyperoptloss + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) longer = hl.hyperopt_loss_function(hyperopt_results, 100) shorter = hl.hyperopt_loss_function(resultsb, 100) assert shorter < longer @@ -310,7 +310,7 @@ def test_loss_calculation_has_limited_profit(default_conf, hyperopt_results) -> results_under = hyperopt_results.copy() results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - hl = HyperOptLossResolver(default_conf).hyperoptloss + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) correct = hl.hyperopt_loss_function(hyperopt_results, 600) over = hl.hyperopt_loss_function(results_over, 600) under = hl.hyperopt_loss_function(results_under, 600) @@ -325,7 +325,7 @@ def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> N results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'}) - hl = HyperOptLossResolver(default_conf).hyperoptloss + 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), @@ -343,7 +343,7 @@ def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'}) - hl = HyperOptLossResolver(default_conf).hyperoptloss + 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), diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 43285cdb1..21929de2b 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -53,7 +53,8 @@ def test_load_pairlist_noexist(mocker, markets, default_conf): with pytest.raises(OperationalException, match=r"Impossible to load Pairlist 'NonexistingPairList'. " r"This class does not exist or contains Python code errors."): - PairListResolver('NonexistingPairList', bot.exchange, plm, default_conf, {}, 1) + PairListResolver.load_pairlist('NonexistingPairList', bot.exchange, plm, + default_conf, {}, 1) def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf): diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 0a8c1cabd..3b897572c 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -29,7 +29,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) @@ -65,7 +65,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'open_order': '(limit buy rem=0.00000000)' } == results[0] - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available"))) # invalidate ticker cache rpc._freqtrade.exchange._cached_ticker = {} @@ -104,7 +104,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) @@ -134,7 +134,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert 'ETH/BTC' == result[0][1] assert '-0.59% (-0.09)' == result[0][3] - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available"))) # invalidate ticker cache rpc._freqtrade.exchange._cached_ticker = {} @@ -149,7 +149,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets) ) @@ -201,7 +201,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) @@ -225,7 +225,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, # Update the ticker with a market going up mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_up + fetch_ticker=ticker_sell_up ) trade.update(limit_sell_order) trade.close_date = datetime.utcnow() @@ -239,7 +239,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, # Update the ticker with a market going up mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_up + fetch_ticker=ticker_sell_up ) trade.update(limit_sell_order) trade.close_date = datetime.utcnow() @@ -260,7 +260,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, assert prec_satoshi(stats['best_rate'], 6.2) # Test non-available pair - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available"))) # invalidate ticker cache rpc._freqtrade.exchange._cached_ticker = {} @@ -287,7 +287,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) @@ -306,7 +306,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, # Update the ticker with a market going up mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_up, + fetch_ticker=ticker_sell_up, get_fee=fee ) trade.update(limit_sell_order) @@ -439,7 +439,7 @@ def test_rpc_start(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock() + fetch_ticker=MagicMock() ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) @@ -460,7 +460,7 @@ def test_rpc_stop(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock() + fetch_ticker=MagicMock() ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) @@ -482,7 +482,7 @@ def test_rpc_stopbuy(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock() + fetch_ticker=MagicMock() ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) @@ -502,7 +502,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: cancel_order_mock = MagicMock() mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, cancel_order=cancel_order_mock, get_order=MagicMock( return_value={ @@ -604,7 +604,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) @@ -637,7 +637,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) @@ -661,7 +661,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order) -> None mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, buy=buy_mm ) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index f1e3421c5..36bb81e41 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -256,7 +256,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets) ) @@ -292,7 +292,7 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets) ) @@ -308,7 +308,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets) ) @@ -323,7 +323,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets) ) @@ -413,7 +413,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets) ) @@ -541,7 +541,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets) ) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index b02f11394..f8b9ca8ab 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -150,7 +150,7 @@ def test_status(default_conf, update, mocker, fee, ticker,) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) msg_mock = MagicMock() @@ -204,7 +204,7 @@ def test_status(default_conf, update, mocker, fee, ticker,) -> None: def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) msg_mock = MagicMock() @@ -254,7 +254,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': 'mocked_order_id'}), get_fee=fee, ) @@ -307,7 +307,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) msg_mock = MagicMock() @@ -373,7 +373,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker + fetch_ticker=ticker ) msg_mock = MagicMock() mocker.patch.multiple( @@ -411,7 +411,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) msg_mock = MagicMock() @@ -443,7 +443,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, msg_mock.reset_mock() # Update the ticker with a market going up - mocker.patch('freqtrade.exchange.Exchange.get_ticker', ticker_sell_up) + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up) trade.update(limit_sell_order) trade.close_date = datetime.utcnow() @@ -534,7 +534,7 @@ def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 - assert "*Warning:*Simulated balances in Dry Mode." in result + assert "*Warning:* Simulated balances in Dry Mode." in result assert "Starting capital: `1000` BTC" in result @@ -700,7 +700,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, patch_whitelist(mocker, default_conf) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) @@ -715,7 +715,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, assert trade # Increase the price and sell it - mocker.patch('freqtrade.exchange.Exchange.get_ticker', ticker_sell_up) + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up) # /forcesell 1 context = MagicMock() @@ -755,7 +755,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) @@ -769,7 +769,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, # Decrease the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_down + fetch_ticker=ticker_sell_down ) trade = Trade.query.first() @@ -812,7 +812,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None patch_whitelist(mocker, default_conf) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) default_conf['max_open_trades'] = 4 @@ -963,7 +963,7 @@ def test_performance_handle(default_conf, update, ticker, fee, ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) @@ -998,7 +998,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': 'mocked_order_id'}), get_fee=fee, ) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 605622b8f..89c38bda1 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -302,6 +302,19 @@ def test_is_pair_locked(default_conf): # 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) + + # Unlocking a pair that's not locked should not raise an error + strategy.unlock_pair(pair) + + # Unlock original pair + pair = 'ETH/BTC' + strategy.unlock_pair(pair) + assert not strategy.is_pair_locked(pair) diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 963d36c76..ce7ac1741 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -39,8 +39,8 @@ def test_load_strategy(default_conf, result): default_conf.update({'strategy': 'SampleStrategy', 'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates') }) - resolver = StrategyResolver(default_conf) - assert 'rsi' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) + strategy = StrategyResolver.load_strategy(default_conf) + assert 'rsi' in strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) def test_load_strategy_base64(result, caplog, default_conf): @@ -48,8 +48,8 @@ def test_load_strategy_base64(result, caplog, default_conf): encoded_string = urlsafe_b64encode(file.read()).decode("utf-8") default_conf.update({'strategy': 'SampleStrategy:{}'.format(encoded_string)}) - resolver = StrategyResolver(default_conf) - assert 'rsi' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) + strategy = StrategyResolver.load_strategy(default_conf) + assert 'rsi' in strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) # Make sure strategy was loaded from base64 (using temp directory)!! assert log_has_re(r"Using resolved strategy SampleStrategy from '" r".*(/|\\).*(/|\\)SampleStrategy\.py'\.\.\.", caplog) @@ -57,13 +57,13 @@ def test_load_strategy_base64(result, caplog, default_conf): def test_load_strategy_invalid_directory(result, caplog, default_conf): default_conf['strategy'] = 'DefaultStrategy' - resolver = StrategyResolver(default_conf) extra_dir = Path.cwd() / 'some/path' - resolver._load_strategy('DefaultStrategy', config=default_conf, extra_dir=extra_dir) + strategy = StrategyResolver._load_strategy('DefaultStrategy', config=default_conf, + extra_dir=extra_dir) assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog) - assert 'rsi' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) + assert 'rsi' in strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) def test_load_not_found_strategy(default_conf): @@ -71,7 +71,7 @@ def test_load_not_found_strategy(default_conf): with pytest.raises(OperationalException, match=r"Impossible to load Strategy 'NotFoundStrategy'. " r"This class does not exist or contains Python code errors."): - StrategyResolver(default_conf) + StrategyResolver.load_strategy(default_conf) def test_load_strategy_noname(default_conf): @@ -79,30 +79,30 @@ def test_load_strategy_noname(default_conf): with pytest.raises(OperationalException, match="No strategy set. Please use `--strategy` to specify " "the strategy class to use."): - StrategyResolver(default_conf) + StrategyResolver.load_strategy(default_conf) def test_strategy(result, default_conf): default_conf.update({'strategy': 'DefaultStrategy'}) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) metadata = {'pair': 'ETH/BTC'} - assert resolver.strategy.minimal_roi[0] == 0.04 + assert strategy.minimal_roi[0] == 0.04 assert default_conf["minimal_roi"]['0'] == 0.04 - assert resolver.strategy.stoploss == -0.10 + assert strategy.stoploss == -0.10 assert default_conf['stoploss'] == -0.10 - assert resolver.strategy.ticker_interval == '5m' + assert strategy.ticker_interval == '5m' assert default_conf['ticker_interval'] == '5m' - df_indicators = resolver.strategy.advise_indicators(result, metadata=metadata) + df_indicators = strategy.advise_indicators(result, metadata=metadata) assert 'adx' in df_indicators - dataframe = resolver.strategy.advise_buy(df_indicators, metadata=metadata) + dataframe = strategy.advise_buy(df_indicators, metadata=metadata) assert 'buy' in dataframe.columns - dataframe = resolver.strategy.advise_sell(df_indicators, metadata=metadata) + dataframe = strategy.advise_sell(df_indicators, metadata=metadata) assert 'sell' in dataframe.columns @@ -114,9 +114,9 @@ def test_strategy_override_minimal_roi(caplog, default_conf): "0": 0.5 } }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.minimal_roi[0] == 0.5 + assert strategy.minimal_roi[0] == 0.5 assert log_has("Override strategy 'minimal_roi' with value in config file: {'0': 0.5}.", caplog) @@ -126,9 +126,9 @@ def test_strategy_override_stoploss(caplog, default_conf): 'strategy': 'DefaultStrategy', 'stoploss': -0.5 }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.stoploss == -0.5 + assert strategy.stoploss == -0.5 assert log_has("Override strategy 'stoploss' with value in config file: -0.5.", caplog) @@ -138,10 +138,10 @@ def test_strategy_override_trailing_stop(caplog, default_conf): 'strategy': 'DefaultStrategy', 'trailing_stop': True }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.trailing_stop - assert isinstance(resolver.strategy.trailing_stop, bool) + assert strategy.trailing_stop + assert isinstance(strategy.trailing_stop, bool) assert log_has("Override strategy 'trailing_stop' with value in config file: True.", caplog) @@ -153,13 +153,13 @@ def test_strategy_override_trailing_stop_positive(caplog, default_conf): 'trailing_stop_positive_offset': -0.2 }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.trailing_stop_positive == -0.1 + assert strategy.trailing_stop_positive == -0.1 assert log_has("Override strategy 'trailing_stop_positive' with value in config file: -0.1.", caplog) - assert resolver.strategy.trailing_stop_positive_offset == -0.2 + assert strategy.trailing_stop_positive_offset == -0.2 assert log_has("Override strategy 'trailing_stop_positive' with value in config file: -0.1.", caplog) @@ -172,10 +172,10 @@ def test_strategy_override_ticker_interval(caplog, default_conf): 'ticker_interval': 60, 'stake_currency': 'ETH' }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.ticker_interval == 60 - assert resolver.strategy.stake_currency == 'ETH' + assert strategy.ticker_interval == 60 + assert strategy.stake_currency == 'ETH' assert log_has("Override strategy 'ticker_interval' with value in config file: 60.", caplog) @@ -187,9 +187,9 @@ def test_strategy_override_process_only_new_candles(caplog, default_conf): 'strategy': 'DefaultStrategy', 'process_only_new_candles': True }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.process_only_new_candles + assert strategy.process_only_new_candles assert log_has("Override strategy 'process_only_new_candles' with value in config file: True.", caplog) @@ -207,11 +207,11 @@ def test_strategy_override_order_types(caplog, default_conf): 'strategy': 'DefaultStrategy', 'order_types': order_types }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.order_types + assert strategy.order_types for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']: - assert resolver.strategy.order_types[method] == order_types[method] + assert strategy.order_types[method] == order_types[method] assert log_has("Override strategy 'order_types' with value in config file:" " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit'," @@ -225,7 +225,7 @@ def test_strategy_override_order_types(caplog, default_conf): with pytest.raises(ImportError, match=r"Impossible to load Strategy 'DefaultStrategy'. " r"Order-types mapping is incomplete."): - StrategyResolver(default_conf) + StrategyResolver.load_strategy(default_conf) def test_strategy_override_order_tif(caplog, default_conf): @@ -240,11 +240,11 @@ def test_strategy_override_order_tif(caplog, default_conf): 'strategy': 'DefaultStrategy', 'order_time_in_force': order_time_in_force }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.order_time_in_force + assert strategy.order_time_in_force for method in ['buy', 'sell']: - assert resolver.strategy.order_time_in_force[method] == order_time_in_force[method] + assert strategy.order_time_in_force[method] == order_time_in_force[method] assert log_has("Override strategy 'order_time_in_force' with value in config file:" " {'buy': 'fok', 'sell': 'gtc'}.", caplog) @@ -257,7 +257,7 @@ def test_strategy_override_order_tif(caplog, default_conf): with pytest.raises(ImportError, match=r"Impossible to load Strategy 'DefaultStrategy'. " r"Order-time-in-force mapping is incomplete."): - StrategyResolver(default_conf) + StrategyResolver.load_strategy(default_conf) def test_strategy_override_use_sell_signal(caplog, default_conf): @@ -265,9 +265,9 @@ def test_strategy_override_use_sell_signal(caplog, default_conf): default_conf.update({ 'strategy': 'DefaultStrategy', }) - resolver = StrategyResolver(default_conf) - assert resolver.strategy.use_sell_signal - assert isinstance(resolver.strategy.use_sell_signal, bool) + strategy = StrategyResolver.load_strategy(default_conf) + assert strategy.use_sell_signal + assert isinstance(strategy.use_sell_signal, bool) # must be inserted to configuration assert 'use_sell_signal' in default_conf['ask_strategy'] assert default_conf['ask_strategy']['use_sell_signal'] @@ -278,10 +278,10 @@ def test_strategy_override_use_sell_signal(caplog, default_conf): 'use_sell_signal': False, }, }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert not resolver.strategy.use_sell_signal - assert isinstance(resolver.strategy.use_sell_signal, bool) + assert not strategy.use_sell_signal + assert isinstance(strategy.use_sell_signal, bool) assert log_has("Override strategy 'use_sell_signal' with value in config file: False.", caplog) @@ -290,9 +290,9 @@ def test_strategy_override_use_sell_profit_only(caplog, default_conf): default_conf.update({ 'strategy': 'DefaultStrategy', }) - resolver = StrategyResolver(default_conf) - assert not resolver.strategy.sell_profit_only - assert isinstance(resolver.strategy.sell_profit_only, bool) + strategy = StrategyResolver.load_strategy(default_conf) + assert not strategy.sell_profit_only + assert isinstance(strategy.sell_profit_only, bool) # must be inserted to configuration assert 'sell_profit_only' in default_conf['ask_strategy'] assert not default_conf['ask_strategy']['sell_profit_only'] @@ -303,10 +303,10 @@ def test_strategy_override_use_sell_profit_only(caplog, default_conf): 'sell_profit_only': True, }, }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.sell_profit_only - assert isinstance(resolver.strategy.sell_profit_only, bool) + assert strategy.sell_profit_only + assert isinstance(strategy.sell_profit_only, bool) assert log_has("Override strategy 'sell_profit_only' with value in config file: True.", caplog) @@ -315,11 +315,11 @@ def test_deprecate_populate_indicators(result, default_conf): default_location = path.join(path.dirname(path.realpath(__file__))) default_conf.update({'strategy': 'TestStrategyLegacy', 'strategy_path': default_location}) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") - indicators = resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) + indicators = strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -328,7 +328,7 @@ def test_deprecate_populate_indicators(result, default_conf): with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") - resolver.strategy.advise_buy(indicators, {'pair': 'ETH/BTC'}) + strategy.advise_buy(indicators, {'pair': 'ETH/BTC'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -337,7 +337,7 @@ def test_deprecate_populate_indicators(result, default_conf): with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") - resolver.strategy.advise_sell(indicators, {'pair': 'ETH_BTC'}) + strategy.advise_sell(indicators, {'pair': 'ETH_BTC'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -349,47 +349,47 @@ def test_call_deprecated_function(result, monkeypatch, default_conf): default_location = path.join(path.dirname(path.realpath(__file__))) default_conf.update({'strategy': 'TestStrategyLegacy', 'strategy_path': default_location}) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) metadata = {'pair': 'ETH/BTC'} # Make sure we are using a legacy function - assert resolver.strategy._populate_fun_len == 2 - assert resolver.strategy._buy_fun_len == 2 - assert resolver.strategy._sell_fun_len == 2 - assert resolver.strategy.INTERFACE_VERSION == 1 + assert strategy._populate_fun_len == 2 + assert strategy._buy_fun_len == 2 + assert strategy._sell_fun_len == 2 + assert strategy.INTERFACE_VERSION == 1 - indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata) + indicator_df = strategy.advise_indicators(result, metadata=metadata) assert isinstance(indicator_df, DataFrame) assert 'adx' in indicator_df.columns - buydf = resolver.strategy.advise_buy(result, metadata=metadata) + buydf = strategy.advise_buy(result, metadata=metadata) assert isinstance(buydf, DataFrame) assert 'buy' in buydf.columns - selldf = resolver.strategy.advise_sell(result, metadata=metadata) + selldf = strategy.advise_sell(result, metadata=metadata) assert isinstance(selldf, DataFrame) assert 'sell' in selldf def test_strategy_interface_versioning(result, monkeypatch, default_conf): default_conf.update({'strategy': 'DefaultStrategy'}) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) metadata = {'pair': 'ETH/BTC'} # Make sure we are using a legacy function - assert resolver.strategy._populate_fun_len == 3 - assert resolver.strategy._buy_fun_len == 3 - assert resolver.strategy._sell_fun_len == 3 - assert resolver.strategy.INTERFACE_VERSION == 2 + assert strategy._populate_fun_len == 3 + assert strategy._buy_fun_len == 3 + assert strategy._sell_fun_len == 3 + assert strategy.INTERFACE_VERSION == 2 - indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata) + indicator_df = strategy.advise_indicators(result, metadata=metadata) assert isinstance(indicator_df, DataFrame) assert 'adx' in indicator_df.columns - buydf = resolver.strategy.advise_buy(result, metadata=metadata) + buydf = strategy.advise_buy(result, metadata=metadata) assert isinstance(buydf, DataFrame) assert 'buy' in buydf.columns - selldf = resolver.strategy.advise_sell(result, metadata=metadata) + selldf = strategy.advise_sell(result, metadata=metadata) assert isinstance(selldf, DataFrame) assert 'sell' in selldf diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 292d53315..6564417b3 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -977,7 +977,7 @@ def test_pairlist_resolving_fallback(mocker): assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] assert config['exchange']['name'] == 'binance' - assert config['datadir'] == str(Path.cwd() / "user_data/data/binance") + assert config['datadir'] == Path.cwd() / "user_data/data/binance" @pytest.mark.parametrize("setting", [ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index cd5c92199..48f8b11c3 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -157,7 +157,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, patch_wallet(mocker, free=default_conf['stake_amount']) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee ) @@ -232,7 +232,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf buy_price = limit_buy_order['price'] mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': buy_price * 0.79, 'ask': buy_price * 0.79, 'last': buy_price * 0.79 @@ -272,7 +272,7 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, buy_price = limit_buy_order['price'] mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': buy_price * 0.85, 'ask': buy_price * 0.85, 'last': buy_price * 0.85 @@ -304,7 +304,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, default_conf['max_open_trades'] = 2 mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -467,7 +467,7 @@ def test_create_trades(default_conf, ticker, limit_buy_order, fee, mocker) -> No patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -501,7 +501,7 @@ def test_create_trades_no_stake_amount(default_conf, ticker, limit_buy_order, patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -519,7 +519,7 @@ def test_create_trades_minimal_amount(default_conf, ticker, limit_buy_order, buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=buy_mock, get_fee=fee, ) @@ -539,7 +539,7 @@ def test_create_trades_too_small_stake_amount(default_conf, ticker, limit_buy_or buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=buy_mock, get_fee=fee, ) @@ -558,7 +558,7 @@ def test_create_trades_limit_reached(default_conf, ticker, limit_buy_order, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_balance=MagicMock(return_value=default_conf['stake_amount']), get_fee=fee, @@ -579,7 +579,7 @@ def test_create_trades_no_pairs_let(default_conf, ticker, limit_buy_order, fee, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -600,7 +600,7 @@ def test_create_trades_no_pairs_in_whitelist(default_conf, ticker, limit_buy_ord patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -639,7 +639,7 @@ def test_create_trades_multiple_trades(default_conf, ticker, default_conf['max_open_trades'] = max_open mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': "12355555"}), get_fee=fee, ) @@ -658,7 +658,7 @@ def test_create_trades_preopen(default_conf, ticker, fee, mocker) -> None: default_conf['max_open_trades'] = 4 mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': "12355555"}), get_fee=fee, ) @@ -684,7 +684,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_order=MagicMock(return_value=limit_buy_order), get_fee=fee, @@ -718,7 +718,7 @@ def test_process_exchange_failures(default_conf, ticker, mocker) -> None: patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(side_effect=TemporaryError) ) sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None) @@ -735,7 +735,7 @@ def test_process_operational_exception(default_conf, ticker, mocker) -> None: patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(side_effect=OperationalException) ) worker = Worker(args=None, config=default_conf) @@ -753,7 +753,7 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, fee, mock patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_order=MagicMock(return_value=limit_buy_order), get_fee=fee, @@ -780,7 +780,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_order=MagicMock(return_value=limit_buy_order), get_fee=fee, @@ -830,7 +830,7 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: refresh_mock = MagicMock() mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(side_effect=TemporaryError), refresh_latest_ohlcv=refresh_mock, ) @@ -853,7 +853,7 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: def test_balance_fully_ask_side(mocker, default_conf) -> None: default_conf['bid_strategy']['ask_last_balance'] = 0.0 freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={'ask': 20, 'last': 10})) assert freqtrade.get_target_bid('ETH/BTC') == 20 @@ -862,7 +862,7 @@ def test_balance_fully_ask_side(mocker, default_conf) -> None: def test_balance_fully_last_side(mocker, default_conf) -> None: default_conf['bid_strategy']['ask_last_balance'] = 1.0 freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={'ask': 20, 'last': 10})) assert freqtrade.get_target_bid('ETH/BTC') == 10 @@ -871,7 +871,7 @@ def test_balance_fully_last_side(mocker, default_conf) -> None: def test_balance_bigger_last_ask(mocker, default_conf) -> None: default_conf['bid_strategy']['ask_last_balance'] = 1.0 freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={'ask': 5, 'last': 10})) assert freqtrade.get_target_bid('ETH/BTC') == 5 @@ -891,7 +891,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: buy_mm = MagicMock(return_value={'id': limit_buy_order['id']}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -1000,7 +1000,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -1100,7 +1100,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -1134,7 +1134,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, sell_mock = MagicMock(return_value={'id': limit_sell_order['id']}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -1177,7 +1177,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -1231,7 +1231,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, assert freqtrade.handle_stoploss_on_exchange(trade) is False # price jumped 2x - mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={ + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': 0.00002344, 'ask': 0.00002346, 'last': 0.00002344 @@ -1271,7 +1271,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -1342,7 +1342,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, edge_conf['dry_run_wallet'] = 999.9 mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -1406,7 +1406,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock) # price goes down 5% - mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={ + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': 0.00001172 * 0.95, 'ask': 0.00001173 * 0.95, 'last': 0.00001172 * 0.95 @@ -1422,7 +1422,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, cancel_order_mock.assert_not_called() # price jumped 2x - mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={ + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': 0.00002344, 'ask': 0.00002346, 'last': 0.00002344 @@ -1662,7 +1662,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mock patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -1702,7 +1702,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -1755,7 +1755,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -1787,7 +1787,7 @@ def test_handle_trade_use_sell_signal( patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -1815,7 +1815,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -1843,7 +1843,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(return_value=limit_buy_order_old), cancel_order=cancel_order_mock, get_fee=fee @@ -1870,7 +1870,7 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, o limit_buy_order_old.update({"status": "canceled"}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(return_value=limit_buy_order_old), cancel_order=cancel_order_mock, get_fee=fee @@ -1897,7 +1897,7 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(side_effect=DependencyException), cancel_order=cancel_order_mock, get_fee=fee @@ -1922,7 +1922,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(return_value=limit_sell_order_old), cancel_order=cancel_order_mock ) @@ -1950,7 +1950,7 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(return_value=limit_sell_order_old), cancel_order=cancel_order_mock ) @@ -1977,7 +1977,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order=cancel_order_mock ) @@ -2004,7 +2004,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order=cancel_order_mock, get_trades_for_order=MagicMock(return_value=trades_for_order), @@ -2041,7 +2041,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order=cancel_order_mock, get_trades_for_order=MagicMock(return_value=trades_for_order), @@ -2085,7 +2085,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(side_effect=requests.exceptions.RequestException('Oh snap')), cancel_order=cancel_order_mock ) @@ -2175,7 +2175,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) patch_whitelist(mocker, default_conf) @@ -2191,7 +2191,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N # Increase the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_up + fetch_ticker=ticker_sell_up ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) @@ -2223,7 +2223,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) patch_whitelist(mocker, default_conf) @@ -2239,7 +2239,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) # Decrease the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_down + fetch_ticker=ticker_sell_down ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], @@ -2273,7 +2273,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) patch_whitelist(mocker, default_conf) @@ -2289,7 +2289,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe # Decrease the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_down + fetch_ticker=ticker_sell_down ) default_conf['dry_run'] = True @@ -2332,7 +2332,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, sell=sellmock ) @@ -2369,7 +2369,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke cancel_order = MagicMock(return_value=True) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, symbol_amount_prec=lambda s, x, y: y, symbol_price_prec=lambda s, x, y: y, @@ -2393,7 +2393,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke # Increase the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_up + fetch_ticker=ticker_sell_up ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], @@ -2412,7 +2412,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, symbol_amount_prec=lambda s, x, y: y, symbol_price_prec=lambda s, x, y: y, @@ -2476,7 +2476,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) patch_whitelist(mocker, default_conf) @@ -2492,7 +2492,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, # Increase the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_up + fetch_ticker=ticker_sell_up ) freqtrade.config['order_types']['sell'] = 'market' @@ -2530,7 +2530,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00002172, 'ask': 0.00002173, 'last': 0.00002172 @@ -2562,7 +2562,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00002172, 'ask': 0.00002173, 'last': 0.00002172 @@ -2592,7 +2592,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00000172, 'ask': 0.00000173, 'last': 0.00000172 @@ -2621,7 +2621,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.0000172, 'ask': 0.0000173, 'last': 0.0000172 @@ -2654,7 +2654,7 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00002172, 'ask': 0.00002173, 'last': 0.00002172 @@ -2728,7 +2728,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -2743,7 +2743,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo # Decrease the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_down + fetch_ticker=ticker_sell_down ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], @@ -2764,7 +2764,7 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.0000172, 'ask': 0.0000173, 'last': 0.0000172 @@ -2798,7 +2798,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001099, 'ask': 0.00001099, 'last': 0.00001099 @@ -2817,7 +2817,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) assert freqtrade.handle_trade(trade) is False # Raise ticker above buy price - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': 0.00001099 * 1.5, 'ask': 0.00001099 * 1.5, @@ -2828,7 +2828,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) assert freqtrade.handle_trade(trade) is False # Price fell - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': 0.00001099 * 1.1, 'ask': 0.00001099 * 1.1, @@ -2852,7 +2852,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': buy_price - 0.000001, 'ask': buy_price - 0.000001, 'last': buy_price - 0.000001 @@ -2876,7 +2876,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, assert freqtrade.handle_trade(trade) is False # Raise ticker above buy price - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': buy_price + 0.000003, 'ask': buy_price + 0.000003, @@ -2888,7 +2888,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, assert log_has(f"ETH/BTC - Adjusting stoploss...", caplog) assert trade.stop_loss == 0.0000138501 - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': buy_price + 0.000002, 'ask': buy_price + 0.000002, @@ -2909,7 +2909,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': buy_price - 0.000001, 'ask': buy_price - 0.000001, 'last': buy_price - 0.000001 @@ -2933,7 +2933,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, assert freqtrade.handle_trade(trade) is False # Raise ticker above buy price - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': buy_price + 0.000003, 'ask': buy_price + 0.000003, @@ -2946,7 +2946,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, assert log_has(f"ETH/BTC - Adjusting stoploss...", caplog) assert trade.stop_loss == 0.0000138501 - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': buy_price + 0.000002, 'ask': buy_price + 0.000002, @@ -2970,7 +2970,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': buy_price, 'ask': buy_price, 'last': buy_price @@ -2997,7 +2997,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, assert trade.stop_loss == 0.0000098910 # Raise ticker above buy price - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': buy_price + 0.0000004, 'ask': buy_price + 0.0000004, @@ -3011,7 +3011,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, assert trade.stop_loss == 0.0000098910 # price rises above the offset (rises 12% when the offset is 5.5%) - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': buy_price + 0.0000014, 'ask': buy_price + 0.0000014, @@ -3031,7 +3031,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00000172, 'ask': 0.00000173, 'last': 0.00000172 @@ -3368,7 +3368,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, mocker.patch('freqtrade.exchange.Exchange.get_order_book', order_book_l2) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -3405,7 +3405,7 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o mocker.patch('freqtrade.exchange.Exchange.get_order_book', order_book_l2) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -3428,7 +3428,7 @@ def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_order_book=order_book_l2, - get_ticker=ticker_mock, + fetch_ticker=ticker_mock, ) default_conf['exchange']['name'] = 'binance' @@ -3452,7 +3452,7 @@ def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_order_book=order_book_l2, - get_ticker=ticker_mock, + fetch_ticker=ticker_mock, ) default_conf['exchange']['name'] = 'binance' @@ -3502,7 +3502,7 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -3533,7 +3533,7 @@ def test_get_sell_rate(default_conf, mocker, ticker, order_book_l2) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_order_book=order_book_l2, - get_ticker=ticker, + fetch_ticker=ticker, ) pair = "ETH/BTC" @@ -3612,7 +3612,7 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) diff --git a/tests/test_integration.py b/tests/test_integration.py index 2daf13db6..11dbca225 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -55,7 +55,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, symbol_amount_prec=lambda s, x, y: y, symbol_price_prec=lambda s, x, y: y, @@ -118,15 +118,13 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc default_conf['max_open_trades'] = 5 default_conf['forcebuy_enable'] = True default_conf['stake_amount'] = 'unlimited' + default_conf['dry_run_wallet'] = 1000 default_conf['exchange']['name'] = 'binance' default_conf['telegram']['enabled'] = True mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock( - side_effect=[1000, 800, 600, 400, 200] - )) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, symbol_amount_prec=lambda s, x, y: y, symbol_price_prec=lambda s, x, y: y, @@ -138,6 +136,14 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc update_trade_state=MagicMock(), _notify_sell=MagicMock(), ) + should_sell_mock = MagicMock(side_effect=[ + SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), + SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL), + SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), + SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), + SellCheckTuple(sell_flag=None, sell_type=SellType.NONE)] + ) + mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) freqtrade = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtrade) @@ -158,3 +164,20 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc for trade in trades: assert trade.stake_amount == 200 + # Reset trade open order id's + trade.open_order_id = None + trades = Trade.get_open_trades() + assert len(trades) == 5 + bals = freqtrade.wallets.get_all_balances() + + freqtrade.process_maybe_execute_sells(trades) + trades = Trade.get_open_trades() + # One trade sold + assert len(trades) == 4 + # Validate that balance of sold trade is not in dry-run balances anymore. + bals2 = freqtrade.wallets.get_all_balances() + assert bals != bals2 + assert len(bals) == 6 + assert len(bals2) == 5 + assert 'LTC' in bals + assert 'LTC' not in bals2 diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 25ad8b6a7..b9a636e1a 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -100,7 +100,7 @@ def test_init_dryrun_db(default_conf, mocker): init(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://' + assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.dryrun.sqlite' @pytest.mark.usefixtures("init_persistence")