Merge remote-tracking branch 'upstream/develop' into backtest_refactor-2

This commit is contained in:
hroff-1902 2019-12-31 02:16:51 +03:00
commit e6a6c22bba
91 changed files with 1747 additions and 1146 deletions

View File

@ -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: |

View File

@ -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 \

View File

@ -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.

View File

@ -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

View File

@ -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).<br> ***Datatype:*** *Positive integer or -1.*
| `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy). <br> ***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). <br> ***Datatype:*** *Positive float or `"unlimited"`.*
@ -47,7 +47,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *String*
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> ***Datatype:*** *String*
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> ***Datatype:*** *Boolean*
| `dry_run_wallet` | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason. <br> ***Datatype:*** *Float*
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.<br>*Defaults to `1000`.* <br> ***Datatype:*** *Float*
| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> ***Datatype:*** *Boolean*
| `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *Dict*
| `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *Float (as ratio)*
@ -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). <br> ***Datatype:*** *Float*
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> ***Datatype:*** *Float*
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> ***Datatype:*** *Boolean*
| `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. <br> ***Datatype:*** *Integer*
| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. <br> ***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. <br> ***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`.* <br> ***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. <br>*Defaults to `false`.* <br> ***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`.* <br> ***Datatype:*** *Float (as ratio)*
| `ask_strategy.use_order_book` | Enable selling of open trades using Order Book Asks. <br> ***Datatype:*** *Boolean*
| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> ***Datatype:*** *Integer*
| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> ***Datatype:*** *Integer*
| `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). <br> ***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). <br>*Defaults to `1`.* <br> ***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). <br>*Defaults to `false`.* <br> ***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) <br> *Defaults to `0`.* <br> ***Datatype:*** *Float (as ratio)*
| `ask_strategy.use_order_book` | Enable selling of open trades using [Order Book Asks](#sell-price-with-orderbook-enabled). <br> ***Datatype:*** *Boolean*
| `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> ***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. <br>*Defaults to `1`.* <br> ***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). <br>*Defaults to `true`.* <br> ***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). <br> ***Datatype:*** *Dict*
| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> ***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.<br> ***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.** <br> ***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.** <br> ***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.** <br> ***Datatype:*** *String*
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> ***Datatype:*** *String*
| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> ***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.<br>**Keep it in secret, do not disclose publicly.** <br> ***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)). <br> ***Datatype:*** *List*
| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#dynamic-pairlists)). <br> ***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) <br> ***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. <br>*Defaults to `true`.* <br> ***Datatype:*** *Boolean*
| `pairlists` | Define one or more pairlists to be used. [More information below](#dynamic-pairlists). <br>*Defaults to `StaticPairList`.* <br> ***Datatype:*** *List of Dicts*
| `telegram.enabled` | Enable the usage of Telegram. <br> ***Datatype:*** *Boolean*
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. **Keep it in secret, do not disclose publicly.** <br> ***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.** <br> ***Datatype:*** *String*
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> ***Datatype:*** *String*
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> ***Datatype:*** *String*
| `webhook.enabled` | Enable usage of Webhook notifications <br> ***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. <br> ***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. <br> ***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. <br> ***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. <br> ***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. <br> ***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. <br> ***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. <br> ***Datatype:*** *String*
| `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details. <br> ***Datatype:*** *Boolean*
| `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details. <br> ***Datatype:*** *IPv4*
| `api_server.listen_port` | Bind Port. See the [API Server documentation](rest-api.md) for more details. <br> ***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.**<br> ***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.**<br> ***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. <br> ***Datatype:*** *String, SQLAlchemy connect string*
| `api_server.listen_port` | Bind Port. See the [API Server documentation](rest-api.md) for more details. <br>***Datatype:*** *Integer between 1024 and 65535*
| `api_server.username` | Username for API server. See the [API Server documentation](rest-api.md) for more details. <br>**Keep it in secret, do not disclose publicly.**<br> ***Datatype:*** *String*
| `api_server.password` | Password for API server. See the [API Server documentation](rest-api.md) for more details. <br>**Keep it in secret, do not disclose publicly.**<br> ***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. <br> ***Datatype:*** *String, SQLAlchemy connect string*
| `initial_state` | Defines the initial application state. More information below. <br>*Defaults to `stopped`.* <br> ***Datatype:*** *Enum, either `stopped` or `running`*
| `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below. <br> ***Datatype:*** *Boolean*
| `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. <br> ***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)
@ -149,6 +150,9 @@ In this case a trade amount is calculated as:
currency_balance / (max_open_trades - current_open_trades)
```
!!! Note "When using Dry-Run Mode"
When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency.
### Understand minimal_roi
The `minimal_roi` configuration parameter is a JSON object where the key is a duration
@ -204,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.
@ -390,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.
@ -501,8 +546,10 @@ creating trades on the exchange.
}
```
Once you will be happy with your bot performance running in the Dry-run mode,
you can switch it to production mode.
Once you will be happy with your bot performance running in the Dry-run mode, you can switch it to production mode.
!!! Note
A simulated wallet is available during dry-run mode, and will assume a starting capital of `dry_run_wallet` (defaults to 1000).
## Switch to production mode
@ -532,7 +579,7 @@ you run it in production mode.
```
!!! Note
If you have an exchange API key yet, [see our tutorial](/pre-requisite).
If you have an exchange API key yet, [see our tutorial](installation.md#setup-your-exchange-account).
You should also make sure to read the [Exchanges](exchanges.md) section of the documentation to be aware of potential configuration details specific to your exchange.

View File

@ -8,6 +8,27 @@ You can analyze the results of backtests and trading history easily using Jupyte
* Don't forget to start a Jupyter notebook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)*
* Copy the example notebook before use so your changes don't get clobbered with the next freqtrade update.
### Using virtual environment with system-wide Jupyter installation
Sometimes it can be desired to use a system-wide installation of Jupyter notebook, and use a jupyter kernel from the virtual environment.
This prevents you from installing the full jupyter suite multiple times per system, and provides an easy way to switch between tasks (freqtrade / other analytics tasks).
For this to work, first activate your virtual environment and run the following commands:
``` bash
# Activate virtual environment
source .env/bin/activate
pip install ipykernel
ipython kernel install --user --name=freqtrade
# Restart jupyter (lab / notebook)
# select kernel "freqtrade" in the notebook
```
!!! Note
This section is provided for completeness, the Freqtrade Team won't provide full support for problems with this setup and will recommend to install Jupyter in the virtual environment directly, as that is the easiest way to get jupyter notebooks up and running. For help with this setup please refer to the [Project Jupyter](https://jupyter.org/) [documentation](https://jupyter.org/documentation) or [help channels](https://jupyter.org/community).
## Fine print
Some tasks don't work especially well in notebooks. For example, anything using asynchronous execution is a problem for Jupyter. Also, freqtrade's primary entry point is the shell cli, so using pure python in a notebook bypasses arguments that provide required objects and parameters to helper functions. You may need to set those values or create expected objects manually.

View File

@ -183,17 +183,19 @@ raw = ct.fetch_ohlcv(pair, timeframe=timeframe)
# convert to dataframe
df1 = parse_ticker_dataframe(raw, timeframe, pair=pair, drop_incomplete=False)
print(df1["date"].tail(1))
print(df1.tail(1))
print(datetime.utcnow())
```
``` output
19 2019-06-08 00:00:00+00:00
date open high low close volume
499 2019-06-08 00:00:00+00:00 0.000007 0.000007 0.000007 0.000007 26264344.0
2019-06-09 12:30:27.873327
```
The output will show the last entry from the Exchange as well as the current UTC date.
If the day shows the same day, then the last candle can be assumed as incomplete and should be dropped (leave the setting `"ohlcv_partial_candle"` from the exchange-class untouched / True). Otherwise, set `"ohlcv_partial_candle"` to `False` to not drop Candles (shown in the example above).
Another way is to run this command multiple times in a row and observe if the volume is changing (while the date remains the same).
## Updating example notebooks
@ -246,6 +248,17 @@ Determine if crucial bugfixes have been made between this commit and the current
git log --oneline --no-decorate --no-merges master..new_release
```
To keep the release-log short, best wrap the full git changelog into a collapsible details secction.
```markdown
<details>
<summary>Expand full changelog</summary>
... Full git changelog
</details>
```
### Create github release / tag
Once the PR against master is merged (best right after merging):
@ -253,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.

View File

@ -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.

View File

@ -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 strategys 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 doesnt change before a position is sold. Lets 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. <br>*Defaults to `false`.* <br> ***Datatype:*** *Boolean*
| `process_throttle_secs` | How often should Edge run in seconds. <br>*Defaults to `3600` (once per hour).* <br> ***Datatype:*** *Integer*
| `calculate_since_number_of_days` | Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy. <br> **Note** that it downloads historical data so increasing this number would lead to slowing down the bot. <br>*Defaults to `7`.* <br> ***Datatype:*** *Integer*
| `capital_available_percentage` | This is the percentage of the total capital on exchange in stake currency. <br>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. <br>*Defaults to `0.5`.* <br> ***Datatype:*** *Float*
| `allowed_risk` | Ratio of allowed risk per trade. <br>*Defaults to `0.01` (1%)).* <br> ***Datatype:*** *Float*
| `stoploss_range_min` | Minimum stoploss. <br>*Defaults to `-0.01`.* <br> ***Datatype:*** *Float*
| `stoploss_range_max` | Maximum stoploss. <br>*Defaults to `-0.10`.* <br> ***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. <br> **Note** than having a smaller step means having a bigger range which could lead to slow calculation. <br> If you set this parameter to -0.001, you then slow down the Edge calculation by a factor of 10. <br>*Defaults to `-0.001`.* <br> ***Datatype:*** *Float*
| `minimum_winrate` | It filters out pairs which don't have at least minimum_winrate. <br>This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio. <br>*Defaults to `0.60`.* <br> ***Datatype:*** *Float*
| `minimum_expectancy` | It filters out pairs which have the expectancy lower than this number. <br>Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return. <br>*Defaults to `0.20`.* <br> ***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. <br>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. <br>*Defaults to `10` (it is highly recommended not to decrease this number).* <br> ***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.<br>**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.).<br>*Defaults to `1440` (one day).* <br> ***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.<br>*Defaults to `false`.* <br> ***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
```

View File

@ -61,3 +61,24 @@ print(res)
```shell
$ pip3 install web3
```
### Send incomplete candles to the strategy
Most exchanges return incomplete candles via their ohlcv / klines interface.
By default, Freqtrade assumes that incomplete candles are returned and removes the last candle assuming it's an incomplete candle.
Whether your exchange returns incomplete candles or not can be checked using [the helper script](developer.md#Incomplete-candles) from the Contributor documentation.
If the exchange does return incomplete candles and you would like to have incomplete candles in your strategy, you can set the following parameter in the configuration file.
``` json
{
"exchange": {
"_ft_has_params": {"ohlcv_partial_candle": false}
}
}
```
!!! Warning "Danger of repainting"
Changing this parameter makes the strategy responsible to avoid repainting and handle this accordingly. Doing this is therefore not recommended, and should only be performed by experienced users who are fully aware of the impact this setting has.

View File

@ -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`).

View File

@ -1,2 +1,2 @@
mkdocs-material==4.5.1
mkdocs-material==4.6.0
mdx_truly_sane_lists==1.2

View File

@ -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 <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`

View File

@ -44,9 +44,9 @@ candles.head()
```python
# Load strategy using values set above
from freqtrade.resolvers import StrategyResolver
strategy = StrategyResolver({'strategy': strategy_name,
strategy = StrategyResolver.load_strategy({'strategy': strategy_name,
'user_data_dir': user_data_dir,
'strategy_path': strategy_location}).strategy
'strategy_path': strategy_location})
# Generate buy/sell signals using strategy
df = strategy.analyze_ticker(candles, {'pair': pair})

View File

@ -108,6 +108,47 @@ With custom user directory
freqtrade new-hyperopt --userdir ~/.freqtrade/ --hyperopt AwesomeHyperopt
```
## List Strategies
Use the `list-strategies` subcommand to see all strategies in one particular directory.
```
freqtrade list-strategies --help
usage: freqtrade list-strategies [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--strategy-path PATH] [-1]
optional arguments:
-h, --help show this help message and exit
--strategy-path PATH Specify additional strategy lookup path.
-1, --one-column Print output in one column.
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. Special values are: 'syslog', 'journald'. See the documentation for more details.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default: `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.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
```
!!! Warning
Using this command will try to load all python files from a directory. This can be a security risk if untrusted files reside in this directory, since all module-level code is executed.
Example: search default strategy directory within userdir
``` bash
freqtrade list-strategies --userdir ~/.freqtrade/
```
Example: search dedicated strategy path
``` bash
freqtrade list-strategies --strategy-path ~/.freqtrade/strategies/
```
## List Exchanges
Use the `list-exchanges` subcommand to see the exchanges available for the bot.

View File

@ -11,34 +11,3 @@ if __version__ == 'develop':
except Exception:
# git not available, ignore
pass
class DependencyException(Exception):
"""
Indicates that an assumed dependency is not met.
This could happen when there is currently not enough money on the account.
"""
class OperationalException(Exception):
"""
Requires manual intervention and will usually stop the bot.
This happens when an exchange returns an unexpected error during runtime
or given configuration is invalid.
"""
class InvalidOrderException(Exception):
"""
This is returned when the order is not valid. Example:
If stoploss on exchange order is hit, then trying to cancel the order
should return this exception.
"""
class TemporaryError(Exception):
"""
Temporary network or exchange related error.
This could happen when an exchange is congested, unavailable, or the user
has networking problems. Usually resolves itself after a time.
"""

View File

@ -30,6 +30,8 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column"]
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
@ -62,7 +64,8 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
"print_json", "hyperopt_show_no_header"]
NO_CONF_REQURIED = ["download-data", "list-timeframes", "list-markets", "list-pairs",
"hyperopt-list", "hyperopt-show", "plot-dataframe", "plot-profit"]
"list-strategies", "hyperopt-list", "hyperopt-show", "plot-dataframe",
"plot-profit"]
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"]
@ -131,8 +134,9 @@ class Arguments:
from freqtrade.utils import (start_create_userdir, start_download_data,
start_hyperopt_list, start_hyperopt_show,
start_list_exchanges, start_list_markets,
start_new_hyperopt, start_new_strategy,
start_list_timeframes, start_test_pairlist, start_trading)
start_list_strategies, start_new_hyperopt,
start_new_strategy, start_list_timeframes,
start_test_pairlist, start_trading)
from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit
subparsers = self.parser.add_subparsers(dest='command',
@ -185,6 +189,15 @@ class Arguments:
build_hyperopt_cmd.set_defaults(func=start_new_hyperopt)
self._build_args(optionlist=ARGS_BUILD_HYPEROPT, parser=build_hyperopt_cmd)
# Add list-strategies subcommand
list_strategies_cmd = subparsers.add_parser(
'list-strategies',
help='Print available strategies.',
parents=[_common_parser],
)
list_strategies_cmd.set_defaults(func=start_list_strategies)
self._build_args(optionlist=ARGS_LIST_STRATEGIES, parser=list_strategies_cmd)
# Add list-exchanges subcommand
list_exchanges_cmd = subparsers.add_parser(
'list-exchanges',

View File

@ -1,9 +1,9 @@
import logging
from typing import Any, Dict
from freqtrade import OperationalException
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason,
is_exchange_known_ccxt, is_exchange_bad,
is_exchange_bad, is_exchange_known_ccxt,
is_exchange_officially_supported)
from freqtrade.state import RunMode

View File

@ -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

View File

@ -4,7 +4,8 @@ from typing import Any, Dict
from jsonschema import Draft4Validator, validators
from jsonschema.exceptions import ValidationError, best_match
from freqtrade import constants, OperationalException
from freqtrade import constants
from freqtrade.exceptions import OperationalException
from freqtrade.state import RunMode
logger = logging.getLogger(__name__)

View File

@ -7,15 +7,16 @@ from copy import deepcopy
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional
from freqtrade import OperationalException, constants
from freqtrade import constants
from freqtrade.configuration.check_exchange import check_exchange
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
from freqtrade.configuration.directory_operations import (create_datadir,
create_userdata_dir)
from freqtrade.configuration.load_config import load_config_file
from freqtrade.exceptions import OperationalException
from freqtrade.loggers import setup_logging
from freqtrade.misc import deep_merge_dicts, json_load
from freqtrade.state import RunMode, TRADING_MODES, NON_UTIL_MODES
from freqtrade.state import NON_UTIL_MODES, TRADING_MODES, RunMode
logger = logging.getLogger(__name__)
@ -223,13 +224,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 +404,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)

View File

@ -5,7 +5,7 @@ Functions to handle deprecated settings
import logging
from typing import Any, Dict
from freqtrade import OperationalException
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__)

View File

@ -3,13 +3,13 @@ import shutil
from pathlib import Path
from typing import Any, Dict, Optional
from freqtrade import OperationalException
from freqtrade.exceptions import OperationalException
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:

View File

@ -6,7 +6,7 @@ import logging
import sys
from typing import Any, Dict
from freqtrade import OperationalException
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__)

View File

@ -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']
@ -18,7 +18,7 @@ REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'PrecisionFilter', 'PriceFilter']
DRY_RUN_WALLET = 999.9
DRY_RUN_WALLET = 1000
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
USERPATH_HYPEROPTS = 'hyperopts'
@ -75,7 +75,7 @@ CONF_SCHEMA = {
},
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
'dry_run': {'type': 'boolean'},
'dry_run_wallet': {'type': 'number'},
'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET},
'process_only_new_candles': {'type': 'boolean'},
'minimal_roi': {
'type': 'object',
@ -275,6 +275,7 @@ CONF_SCHEMA = {
'stake_currency',
'stake_amount',
'dry_run',
'dry_run_wallet',
'bid_strategy',
'unfilledtimeout',
'stoploss',

View File

@ -108,7 +108,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
trades = pd.DataFrame([(t.pair,
t.open_date.replace(tzinfo=timezone.utc),
t.close_date.replace(tzinfo=timezone.utc) if t.close_date else None,
t.calc_profit(), t.calc_profit_percent(),
t.calc_profit(), t.calc_profit_ratio(),
t.open_rate, t.close_rate, t.amount,
(round((t.close_date.timestamp() - t.open_date.timestamp()) / 60, 2)
if t.close_date else None),

View File

@ -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:

View File

@ -16,10 +16,12 @@ from typing import Any, Dict, List, Optional, Tuple
import arrow
from pandas import DataFrame
from freqtrade import OperationalException, misc
from freqtrade import misc
from freqtrade.configuration import TimeRange
from freqtrade.data.converter import parse_ticker_dataframe, trades_to_ohlcv
from freqtrade.exchange import Exchange, timeframe_to_minutes, timeframe_to_seconds
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import (Exchange, timeframe_to_minutes,
timeframe_to_seconds)
logger = logging.getLogger(__name__)
@ -68,7 +70,7 @@ def trim_dataframe(df: DataFrame, timerange: TimeRange, df_date_col: str = 'date
def load_tickerdata_file(datadir: Path, pair: str, timeframe: str,
timerange: Optional[TimeRange] = None) -> Optional[list]:
timerange: Optional[TimeRange] = None) -> List[Dict]:
"""
Load a pair from file, either .json.gz or .json
:return: tickerlist or None if unsuccessful
@ -128,39 +130,26 @@ def load_pair_history(pair: str,
timeframe: str,
datadir: Path,
timerange: Optional[TimeRange] = None,
refresh_pairs: bool = False,
exchange: Optional[Exchange] = None,
fill_up_missing: bool = True,
drop_incomplete: bool = True,
startup_candles: int = 0,
) -> DataFrame:
"""
Loads cached ticker history for the given pair.
Load cached ticker history for the given pair.
:param pair: Pair to load data for
:param timeframe: Ticker timeframe (e.g. "5m")
:param datadir: Path to the data storage location.
:param timerange: Limit data to be loaded to this timerange
:param refresh_pairs: Refresh pairs from exchange.
(Note: Requires exchange to be passed as well.)
:param exchange: Exchange object (needed when using "refresh_pairs")
:param fill_up_missing: Fill missing values with "No action"-candles
:param drop_incomplete: Drop last candle assuming it may be incomplete.
:param startup_candles: Additional candles to load at the start of the period
:return: DataFrame with ohlcv data, or empty DataFrame
"""
timerange_startup = deepcopy(timerange)
if startup_candles > 0 and timerange_startup:
timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles)
# The user forced the refresh of pairs
if refresh_pairs:
download_pair_history(datadir=datadir,
exchange=exchange,
pair=pair,
timeframe=timeframe,
timerange=timerange)
pairdata = load_tickerdata_file(datadir, pair, timeframe, timerange=timerange_startup)
if pairdata:
@ -180,30 +169,22 @@ def load_pair_history(pair: str,
def load_data(datadir: Path,
timeframe: str,
pairs: List[str],
refresh_pairs: bool = False,
exchange: Optional[Exchange] = None,
timerange: Optional[TimeRange] = None,
fill_up_missing: bool = True,
startup_candles: int = 0,
fail_without_data: bool = False
) -> Dict[str, DataFrame]:
"""
Loads ticker history data for a list of pairs
Load ticker history data for a list of pairs.
:param datadir: Path to the data storage location.
:param timeframe: Ticker Timeframe (e.g. "5m")
:param pairs: List of pairs to load
:param refresh_pairs: Refresh pairs from exchange.
(Note: Requires exchange to be passed as well.)
:param exchange: Exchange object (needed when using "refresh_pairs")
:param timerange: Limit data to be loaded to this timerange
:param fill_up_missing: Fill missing values with "No action"-candles
:param startup_candles: Additional candles to load at the start of the period
:param fail_without_data: Raise OperationalException if no data is found.
:return: dict(<pair>:<Dataframe>)
TODO: refresh_pairs is still used by edge to keep the data uptodate.
This should be replaced in the future. Instead, writing the current candles to disk
from dataprovider should be implemented, as this would avoid loading ohlcv data twice.
exchange and refresh_pairs are then not needed here nor in load_pair_history.
"""
result: Dict[str, DataFrame] = {}
if startup_candles > 0 and timerange:
@ -212,8 +193,6 @@ def load_data(datadir: Path,
for pair in pairs:
hist = load_pair_history(pair=pair, timeframe=timeframe,
datadir=datadir, timerange=timerange,
refresh_pairs=refresh_pairs,
exchange=exchange,
fill_up_missing=fill_up_missing,
startup_candles=startup_candles)
if not hist.empty:
@ -224,6 +203,27 @@ def load_data(datadir: Path,
return result
def refresh_data(datadir: Path,
timeframe: str,
pairs: List[str],
exchange: Exchange,
timerange: Optional[TimeRange] = None,
) -> None:
"""
Refresh ticker history data for a list of pairs.
:param datadir: Path to the data storage location.
:param timeframe: Ticker Timeframe (e.g. "5m")
:param pairs: List of pairs to load
:param exchange: Exchange object
:param timerange: Limit data to be loaded to this timerange
"""
for pair in pairs:
_download_pair_history(pair=pair, timeframe=timeframe,
datadir=datadir, timerange=timerange,
exchange=exchange)
def pair_data_filename(datadir: Path, pair: str, timeframe: str) -> Path:
pair_s = pair.replace("/", "_")
filename = datadir.joinpath(f'{pair_s}-{timeframe}.json')
@ -277,8 +277,8 @@ def _load_cached_data_for_updating(datadir: Path, pair: str, timeframe: str,
return (data, since_ms)
def download_pair_history(datadir: Path,
exchange: Optional[Exchange],
def _download_pair_history(datadir: Path,
exchange: Exchange,
pair: str,
timeframe: str = '5m',
timerange: Optional[TimeRange] = None) -> bool:
@ -295,11 +295,6 @@ def download_pair_history(datadir: Path,
:param timerange: range of time to download
:return: bool with success state
"""
if not exchange:
raise OperationalException(
"Exchange needs to be initialized when downloading pair history data"
)
try:
logger.info(
f'Download history data for pair: "{pair}", timeframe: {timeframe} '
@ -312,11 +307,12 @@ def download_pair_history(datadir: Path,
logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None')
# Default since_ms to 30 days if nothing is given
new_data = exchange.get_historic_ohlcv(pair=pair, timeframe=timeframe,
since_ms=since_ms if since_ms
else
new_data = exchange.get_historic_ohlcv(pair=pair,
timeframe=timeframe,
since_ms=since_ms if since_ms else
int(arrow.utcnow().shift(
days=-30).float_timestamp) * 1000)
days=-30).float_timestamp) * 1000
)
data.extend(new_data)
logger.debug("New Start: %s", misc.format_ms_time(data[0][0]))
@ -334,12 +330,12 @@ def download_pair_history(datadir: Path,
def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str],
dl_path: Path, timerange: Optional[TimeRange] = None,
datadir: Path, timerange: Optional[TimeRange] = None,
erase=False) -> List[str]:
"""
Refresh stored ohlcv data for backtesting and hyperopt operations.
Used by freqtrade download-data
:return: Pairs not available
Used by freqtrade download-data subcommand.
:return: List of pairs that are not available.
"""
pairs_not_available = []
for pair in pairs:
@ -349,20 +345,20 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
continue
for timeframe in timeframes:
dl_file = pair_data_filename(dl_path, pair, timeframe)
dl_file = pair_data_filename(datadir, pair, timeframe)
if erase and dl_file.exists():
logger.info(
f'Deleting existing data for pair {pair}, interval {timeframe}.')
dl_file.unlink()
logger.info(f'Downloading pair {pair}, interval {timeframe}.')
download_pair_history(datadir=dl_path, exchange=exchange,
_download_pair_history(datadir=datadir, exchange=exchange,
pair=pair, timeframe=str(timeframe),
timerange=timerange)
return pairs_not_available
def download_trades_history(datadir: Path,
def _download_trades_history(datadir: Path,
exchange: Exchange,
pair: str,
timerange: Optional[TimeRange] = None) -> bool:
@ -381,11 +377,11 @@ def download_trades_history(datadir: Path,
logger.debug("Current Start: %s", trades[0]['datetime'] if trades else 'None')
logger.debug("Current End: %s", trades[-1]['datetime'] if trades else 'None')
# Default since_ms to 30 days if nothing is given
new_trades = exchange.get_historic_trades(pair=pair,
since=since if since else
int(arrow.utcnow().shift(
days=-30).float_timestamp) * 1000,
# until=xxx,
from_id=from_id,
)
trades.extend(new_trades[1])
@ -407,9 +403,9 @@ def download_trades_history(datadir: Path,
def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path,
timerange: TimeRange, erase=False) -> List[str]:
"""
Refresh stored trades data.
Used by freqtrade download-data
:return: Pairs not available
Refresh stored trades data for backtesting and hyperopt operations.
Used by freqtrade download-data subcommand.
:return: List of pairs that are not available.
"""
pairs_not_available = []
for pair in pairs:
@ -425,7 +421,7 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir:
dl_file.unlink()
logger.info(f'Downloading trades for pair {pair}.')
download_trades_history(datadir=datadir, exchange=exchange,
_download_trades_history(datadir=datadir, exchange=exchange,
pair=pair,
timerange=timerange)
return pairs_not_available
@ -448,18 +444,19 @@ def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str],
store_tickerdata_file(datadir, pair, timeframe, data=ohlcv)
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
def get_timerange(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
"""
Get the maximum timeframe for the given backtest data
Get the maximum common timerange for the given backtest data.
:param data: dictionary with preprocessed backtesting data
:return: tuple containing min_date, max_date
"""
timeframe = [
timeranges = [
(arrow.get(frame['date'].min()), arrow.get(frame['date'].max()))
for frame in data.values()
]
return min(timeframe, key=operator.itemgetter(0))[0], \
max(timeframe, key=operator.itemgetter(1))[1]
return (min(timeranges, key=operator.itemgetter(0))[0],
max(timeranges, key=operator.itemgetter(1))[1])
def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime,

View File

@ -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
@ -9,12 +8,12 @@ import numpy as np
import utils_find_1st as utf1st
from pandas import DataFrame
from freqtrade import constants, OperationalException
from freqtrade import constants
from freqtrade.configuration import TimeRange
from freqtrade.data import history
from freqtrade.exceptions import OperationalException
from freqtrade.strategy.interface import SellType
logger = logging.getLogger(__name__)
@ -94,12 +93,19 @@ class Edge:
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
logger.info('Using local backtesting data (using whitelist in given config) ...')
if self._refresh_pairs:
history.refresh_data(
datadir=self.config['datadir'],
pairs=pairs,
exchange=self.exchange,
timeframe=self.strategy.ticker_interval,
timerange=self._timerange,
)
data = history.load_data(
datadir=Path(self.config['datadir']),
datadir=self.config['datadir'],
pairs=pairs,
timeframe=self.strategy.ticker_interval,
refresh_pairs=self._refresh_pairs,
exchange=self.exchange,
timerange=self._timerange,
startup_candles=self.strategy.startup_candle_count,
)
@ -113,7 +119,7 @@ class Edge:
preprocessed = self.strategy.tickerdata_to_dataframe(data)
# Print timeframe
min_date, max_date = history.get_timeframe(preprocessed)
min_date, max_date = history.get_timerange(preprocessed)
logger.info(
'Measuring data from %s up to %s (%s days) ...',
min_date.isoformat(),

37
freqtrade/exceptions.py Normal file
View File

@ -0,0 +1,37 @@
class FreqtradeException(Exception):
"""
Freqtrade base exception. Handled at the outermost level.
All other exception types are subclasses of this exception type.
"""
class OperationalException(FreqtradeException):
"""
Requires manual intervention and will stop the bot.
Most of the time, this is caused by an invalid Configuration.
"""
class DependencyException(FreqtradeException):
"""
Indicates that an assumed dependency is not met.
This could happen when there is currently not enough money on the account.
"""
class InvalidOrderException(FreqtradeException):
"""
This is returned when the order is not valid. Example:
If stoploss on exchange order is hit, then trying to cancel the order
should return this exception.
"""
class TemporaryError(FreqtradeException):
"""
Temporary network or exchange related error.
This could happen when an exchange is congested, unavailable, or the user
has networking problems. Usually resolves itself after a time.
"""

View File

@ -4,7 +4,7 @@ from typing import Dict
import ccxt
from freqtrade import (DependencyException, InvalidOrderException,
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.exchange import Exchange

View File

@ -1,6 +1,6 @@
import logging
from freqtrade import DependencyException, TemporaryError
from freqtrade.exceptions import DependencyException, TemporaryError
logger = logging.getLogger(__name__)

View File

@ -17,9 +17,9 @@ import ccxt.async_support as ccxt_async
from ccxt.base.decimal_to_precision import ROUND_DOWN, ROUND_UP
from pandas import DataFrame
from freqtrade import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError, constants)
from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async
from freqtrade.misc import deep_merge_dicts
@ -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."
@ -479,7 +487,7 @@ class Exchange:
@retrier
def get_balance(self, currency: str) -> float:
if self._config['dry_run']:
return constants.DRY_RUN_WALLET
return self._config['dry_run_wallet']
# ccxt exception is already handled by get_balances
balances = self.get_balances()
@ -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'):

View File

@ -4,7 +4,7 @@ from typing import Dict
import ccxt
from freqtrade import OperationalException, TemporaryError
from freqtrade.exceptions import OperationalException, TemporaryError
from freqtrade.exchange import Exchange
from freqtrade.exchange.exchange import retrier

View File

@ -12,17 +12,17 @@ from typing import Any, Dict, List, Optional, Tuple
import arrow
from requests.exceptions import RequestException
from freqtrade import (DependencyException, InvalidOrderException, __version__,
constants, persistence)
from freqtrade import __version__, constants, persistence
from freqtrade.configuration import validate_config_consistency
from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge
from freqtrade.exceptions import DependencyException, InvalidOrderException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
from freqtrade.pairlist.pairlistmanager import PairListManager
from freqtrade.persistence import Trade
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.rpc import RPCManager, RPCMessageType
from freqtrade.pairlist.pairlistmanager import PairListManager
from freqtrade.state import State
from freqtrade.strategy.interface import IStrategy, SellType
from freqtrade.wallets import Wallets
@ -55,14 +55,18 @@ 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))
self.wallets = Wallets(self.config, self.exchange)
self.dataprovider = DataProvider(self.config, self.exchange)
# Attach Dataprovider to Strategy baseclass
@ -78,9 +82,6 @@ class FreqtradeBot:
self.active_pair_whitelist = self._refresh_whitelist()
persistence.init(self.config.get('db_url', None),
clean_open_orders=self.config.get('dry_run', False))
# Set initial bot state from config
initial_state = self.config.get('initial_state')
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
@ -135,7 +136,7 @@ class FreqtradeBot:
self.process_maybe_execute_sells(trades)
# Then looking for buy opportunities
if len(trades) < self.config['max_open_trades']:
if self.get_free_open_trades():
self.process_maybe_execute_buys()
# Check and handle any timed out open orders
@ -172,6 +173,14 @@ class FreqtradeBot:
"""
return [(pair, self.config['ticker_interval']) for pair in pairs]
def get_free_open_trades(self):
"""
Return the number of free open trades slots or 0 if
max number of open trades reached
"""
open_trades = len(Trade.get_open_trades())
return max(0, self.config['max_open_trades'] - open_trades)
def get_target_bid(self, pair: str, tick: Dict = None) -> float:
"""
Calculates bid target between current ask price and last price
@ -191,7 +200,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']:
@ -203,14 +212,14 @@ class FreqtradeBot:
return used_rate
def _get_trade_stake_amount(self, pair) -> Optional[float]:
def get_trade_stake_amount(self, pair) -> Optional[float]:
"""
Check if stake amount can be fulfilled with the available balance
for the stake currency
:return: float: Stake Amount
Calculate stake amount for the trade
:return: float: Stake amount
"""
stake_amount: Optional[float]
if self.edge:
return self.edge.stake_amount(
stake_amount = self.edge.stake_amount(
pair,
self.wallets.get_free(self.config['stake_currency']),
self.wallets.get_total(self.config['stake_currency']),
@ -218,21 +227,34 @@ class FreqtradeBot:
)
else:
stake_amount = self.config['stake_amount']
if stake_amount == constants.UNLIMITED_STAKE_AMOUNT:
stake_amount = self._calculate_unlimited_stake_amount()
return self._check_available_stake_amount(stake_amount)
def _calculate_unlimited_stake_amount(self) -> Optional[float]:
"""
Calculate stake amount for "unlimited" stake amount
:return: None if max number of trades reached
"""
free_open_trades = self.get_free_open_trades()
if not free_open_trades:
return None
available_amount = self.wallets.get_free(self.config['stake_currency'])
return available_amount / free_open_trades
def _check_available_stake_amount(self, stake_amount: Optional[float]) -> Optional[float]:
"""
Check if stake amount can be fulfilled with the available balance
for the stake currency
:return: float: Stake amount
"""
available_amount = self.wallets.get_free(self.config['stake_currency'])
if stake_amount == constants.UNLIMITED_STAKE_AMOUNT:
open_trades = len(Trade.get_open_trades())
if open_trades >= self.config['max_open_trades']:
logger.warning("Can't open a new trade: max number of trades is reached")
return None
return available_amount / (self.config['max_open_trades'] - open_trades)
# Check if stake_amount is fulfilled
if available_amount < stake_amount:
if stake_amount is not None and available_amount < stake_amount:
raise DependencyException(
f"Available balance({available_amount} {self.config['stake_currency']}) is "
f"lower than stake amount({stake_amount} {self.config['stake_currency']})"
f"Available balance ({available_amount} {self.config['stake_currency']}) is "
f"lower than stake amount ({stake_amount} {self.config['stake_currency']})"
)
return stake_amount
@ -298,18 +320,23 @@ class FreqtradeBot:
buycount = 0
# running get_signal on historical data fetched
for _pair in whitelist:
if self.strategy.is_pair_locked(_pair):
logger.info(f"Pair {_pair} is currently locked.")
for pair in whitelist:
if self.strategy.is_pair_locked(pair):
logger.info(f"Pair {pair} is currently locked.")
continue
(buy, sell) = self.strategy.get_signal(
_pair, self.strategy.ticker_interval,
self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval))
pair, self.strategy.ticker_interval,
self.dataprovider.ohlcv(pair, self.strategy.ticker_interval))
if buy and not sell and len(Trade.get_open_trades()) < self.config['max_open_trades']:
stake_amount = self._get_trade_stake_amount(_pair)
if buy and not sell:
if not self.get_free_open_trades():
logger.debug("Can't open a new trade: max number of trades is reached")
continue
stake_amount = self.get_trade_stake_amount(pair)
if not stake_amount:
logger.debug("Stake amount is 0, ignoring possible trade for {pair}.")
continue
logger.info(f"Buy signal found: about create a new trade with stake_amount: "
@ -319,11 +346,11 @@ class FreqtradeBot:
get('check_depth_of_market', {})
if (bidstrat_check_depth_of_market.get('enabled', False)) and\
(bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0):
if self._check_depth_of_market_buy(_pair, bidstrat_check_depth_of_market):
buycount += self.execute_buy(_pair, stake_amount)
if self._check_depth_of_market_buy(pair, bidstrat_check_depth_of_market):
buycount += self.execute_buy(pair, stake_amount)
continue
buycount += self.execute_buy(_pair, stake_amount)
buycount += self.execute_buy(pair, stake_amount)
return buycount > 0
@ -350,7 +377,6 @@ class FreqtradeBot:
:param pair: pair for which we want to create a LIMIT_BUY
:return: None
"""
pair_s = pair.replace('_', '/')
stake_currency = self.config['stake_currency']
fiat_currency = self.config.get('fiat_display_currency', None)
time_in_force = self.strategy.order_time_in_force['buy']
@ -361,10 +387,10 @@ class FreqtradeBot:
# Calculate amount
buy_limit_requested = self.get_target_bid(pair)
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit_requested)
min_stake_amount = self._get_min_pair_stake_amount(pair, buy_limit_requested)
if min_stake_amount is not None and min_stake_amount > stake_amount:
logger.warning(
f"Can't open a new trade for {pair_s}: stake amount "
f"Can't open a new trade for {pair}: stake amount "
f"is too small ({stake_amount} < {min_stake_amount})"
)
return False
@ -387,7 +413,7 @@ class FreqtradeBot:
if float(order['filled']) == 0:
logger.warning('Buy %s order with time in force %s for %s is %s by %s.'
' zero amount is fulfilled.',
order_tif, order_type, pair_s, order_status, self.exchange.name)
order_tif, order_type, pair, order_status, self.exchange.name)
return False
else:
# the order is partially fulfilled
@ -395,7 +421,7 @@ class FreqtradeBot:
# if the order is fulfilled fully or partially
logger.warning('Buy %s order with time in force %s for %s is %s by %s.'
' %s amount fulfilled out of %s (%s remaining which is canceled).',
order_tif, order_type, pair_s, order_status, self.exchange.name,
order_tif, order_type, pair, order_status, self.exchange.name,
order['filled'], order['amount'], order['remaining']
)
stake_amount = order['cost']
@ -412,7 +438,7 @@ class FreqtradeBot:
self.rpc.send_msg({
'type': RPCMessageType.BUY_NOTIFICATION,
'exchange': self.exchange.name.capitalize(),
'pair': pair_s,
'pair': pair,
'limit': buy_limit_filled_price,
'order_type': order_type,
'stake_amount': stake_amount,
@ -554,6 +580,7 @@ class FreqtradeBot:
order['amount'] = new_amount
# Fee was applied, so set to 0
trade.fee_open = 0
trade.recalc_open_trade_price()
except DependencyException as exception:
logger.warning("Could not update trade amount: %s", exception)
@ -568,7 +595,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
"""
@ -580,7 +607,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:
@ -849,6 +876,7 @@ class FreqtradeBot:
trade.amount = new_amount
# Fee was applied, so set to 0
trade.fee_open = 0
trade.recalc_open_trade_price()
except DependencyException as e:
logger.warning("Could not update trade amount: %s", e)
@ -889,6 +917,27 @@ class FreqtradeBot:
# TODO: figure out how to handle partially complete sell orders
return False
def _safe_sell_amount(self, pair: str, amount: float) -> float:
"""
Get sellable amount.
Should be trade.amount - but will fall back to the available amount if necessary.
This should cover cases where get_real_amount() was not able to update the amount
for whatever reason.
:param pair: Pair we're trying to sell
:param amount: amount we expect to be available
:return: amount to sell
:raise: DependencyException: if available balance is not within 2% of the available amount.
"""
wallet_amount = self.wallets.get_free(pair.split('/')[0])
logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}")
if wallet_amount > amount:
return amount
elif wallet_amount > amount * 0.98:
logger.info(f"{pair} - Falling back to wallet-amount.")
return wallet_amount
else:
raise DependencyException("Not enough amount to sell.")
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> None:
"""
Executes a limit sell for the given trade and limit
@ -919,10 +968,12 @@ class FreqtradeBot:
# Emergencysells (default to market!)
ordertype = self.strategy.order_types.get("emergencysell", "market")
amount = self._safe_sell_amount(trade.pair, trade.amount)
# Execute sell and update trade record
order = self.exchange.sell(pair=str(trade.pair),
ordertype=ordertype,
amount=trade.amount, rate=limit,
amount=amount, rate=limit,
time_in_force=self.strategy.order_time_in_force['sell']
)
@ -947,7 +998,7 @@ class FreqtradeBot:
profit_trade = trade.calc_profit(rate=profit_rate)
# Use cached ticker here - it was updated seconds ago.
current_rate = self.get_sell_rate(trade.pair, False)
profit_percent = trade.calc_profit_percent(profit_rate)
profit_percent = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_percent > 0 else "loss"
msg = {

View File

@ -5,7 +5,7 @@ from logging import Formatter
from logging.handlers import RotatingFileHandler, SysLogHandler
from typing import Any, Dict, List
from freqtrade import OperationalException
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__)

View File

@ -4,6 +4,7 @@ Main Freqtrade bot script.
Read the documentation to know what cli arguments you need.
"""
from freqtrade.exceptions import FreqtradeException, OperationalException
import sys
# check min. python version
if sys.version_info < (3, 6):
@ -13,7 +14,6 @@ if sys.version_info < (3, 6):
import logging
from typing import Any, List
from freqtrade import OperationalException
from freqtrade.configuration import Arguments
@ -50,7 +50,7 @@ def main(sysargv: List[str] = None) -> None:
except KeyboardInterrupt:
logger.info('SIGINT received, aborting ...')
return_code = 0
except OperationalException as e:
except FreqtradeException as e:
logger.error(str(e))
return_code = 2
except Exception:

View File

@ -1,11 +1,11 @@
import logging
from typing import Any, Dict
from freqtrade import DependencyException, constants, OperationalException
from freqtrade import constants
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.state import RunMode
from freqtrade.utils import setup_utils_configuration
logger = logging.getLogger(__name__)

View File

@ -12,11 +12,11 @@ from typing import Any, Dict, List, NamedTuple, Optional
from pandas import DataFrame
from tabulate import tabulate
from freqtrade import OperationalException
from freqtrade.configuration import (TimeRange, remove_credentials,
validate_config_consistency)
from freqtrade.data import history
from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.misc import file_dump_json
from freqtrade.persistence import Trade
@ -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,
@ -117,7 +117,7 @@ class Backtesting:
fail_without_data=True,
)
min_date, max_date = history.get_timeframe(data)
min_date, max_date = history.get_timerange(data)
logger.info(
'Loading data from %s up to %s (%s days)..',
@ -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:
@ -346,7 +348,7 @@ class Backtesting:
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
return BacktestResult(pair=pair,
profit_percent=trade.calc_profit_percent(rate=closerate),
profit_percent=trade.calc_profit_ratio(rate=closerate),
profit_abs=trade.calc_profit(rate=closerate),
open_time=buy_row.date,
close_time=sell_row.date,
@ -362,7 +364,7 @@ class Backtesting:
# no sell condition found - trade stil open at end of backtest period
sell_row = partial_ticker[-1]
bt_res = BacktestResult(pair=pair,
profit_percent=trade.calc_profit_percent(rate=sell_row.open),
profit_percent=trade.calc_profit_ratio(rate=sell_row.open),
profit_abs=trade.calc_profit(rate=sell_row.open),
open_time=buy_row.date,
close_time=sell_row.date,
@ -510,7 +512,7 @@ class Backtesting:
# Trim startup period from analyzed dataframe
for pair, df in preprocessed.items():
preprocessed[pair] = history.trim_dataframe(df, timerange)
min_date, max_date = history.get_timeframe(preprocessed)
min_date, max_date = history.get_timerange(preprocessed)
logger.info(
'Backtesting with data from %s up to %s (%s days)..',

View File

@ -12,8 +12,7 @@ from freqtrade import constants
from freqtrade.configuration import (TimeRange, remove_credentials,
validate_config_consistency)
from freqtrade.edge import Edge
from freqtrade.exchange import Exchange
from freqtrade.resolvers import StrategyResolver
from freqtrade.resolvers import StrategyResolver, ExchangeResolver
logger = logging.getLogger(__name__)
@ -33,8 +32,8 @@ class EdgeCli:
# Reset keys for edge
remove_credentials(self.config)
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
self.exchange = Exchange(self.config)
self.strategy = StrategyResolver(self.config).strategy
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
self.strategy = StrategyResolver.load_strategy(self.config)
validate_config_consistency(self.config)
@ -42,11 +41,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')

View File

@ -22,13 +22,13 @@ from joblib import (Parallel, cpu_count, delayed, dump, load,
wrap_non_picklable_objects)
from pandas import DataFrame
from freqtrade import OperationalException
from freqtrade.data.history import get_timeframe, trim_dataframe
from freqtrade.data.history import get_timerange, trim_dataframe
from freqtrade.exceptions import OperationalException
from freqtrade.misc import plural, round_dict
from freqtrade.optimize.backtesting import Backtesting
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F4
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
from freqtrade.resolvers.hyperopt_resolver import (HyperOptLossResolver,
HyperOptResolver)
@ -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'] /
@ -369,7 +369,7 @@ class Hyperopt:
processed = load(self.tickerdata_pickle)
min_date, max_date = get_timeframe(processed)
min_date, max_date = get_timerange(processed)
backtesting_results = self.backtesting.backtest(
processed=processed,
@ -488,7 +488,7 @@ class Hyperopt:
# Trim startup period from analyzed dataframe
for pair, df in preprocessed.items():
preprocessed[pair] = trim_dataframe(df, timerange)
min_date, max_date = get_timeframe(data)
min_date, max_date = get_timerange(data)
logger.info(
'Hyperopting with data from %s up to %s (%s days)..',

View File

@ -4,17 +4,15 @@ This module defines the interface to apply for hyperopt
"""
import logging
import math
from abc import ABC
from typing import Dict, Any, Callable, List
from typing import Any, Callable, Dict, List
from skopt.space import Categorical, Dimension, Integer, Real
from freqtrade import OperationalException
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import round_dict
logger = logging.getLogger(__name__)

View File

@ -8,7 +8,7 @@ import logging
from datetime import datetime
from typing import Dict, List
from freqtrade import OperationalException
from freqtrade.exceptions import OperationalException
from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__)

View File

@ -4,11 +4,12 @@ Static List provider
Provides lists as configured in config.json
"""
from cachetools import TTLCache, cached
import logging
from typing import Dict, List
from freqtrade import OperationalException
from cachetools import TTLCache, cached
from freqtrade.exceptions import OperationalException
from freqtrade.pairlist.IPairList import IPairList
from freqtrade.resolvers import PairListResolver
@ -28,13 +29,13 @@ class PairListManager():
if 'method' not in pl:
logger.warning(f"No method in {pl}")
continue
pairl = PairListResolver(pl.get('method'),
pairl = PairListResolver.load_pairlist(pl.get('method'),
exchange=exchange,
pairlistmanager=self,
config=config,
pairlistconfig=pl,
pairlist_pos=len(self._pairlists)
).pairlist
)
self._tickers_needed = pairl.needstickers or self._tickers_needed
self._pairlists.append(pairl)

View File

@ -16,7 +16,7 @@ from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.pool import StaticPool
from freqtrade import OperationalException
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__)
@ -86,7 +86,7 @@ def check_migrate(engine) -> None:
logger.debug(f'trying {table_back_name}')
# Check for latest column
if not has_column(cols, 'stop_loss_pct'):
if not has_column(cols, 'open_trade_price'):
logger.info(f'Running database migration - backup available as {table_back_name}')
fee_open = get_column_def(cols, 'fee_open', 'fee')
@ -104,6 +104,8 @@ def check_migrate(engine) -> None:
sell_reason = get_column_def(cols, 'sell_reason', 'null')
strategy = get_column_def(cols, 'strategy', 'null')
ticker_interval = get_column_def(cols, 'ticker_interval', 'null')
open_trade_price = get_column_def(cols, 'open_trade_price',
f'amount * open_rate * (1 + {fee_open})')
# Schema migration necessary
engine.execute(f"alter table trades rename to {table_back_name}")
@ -121,7 +123,7 @@ def check_migrate(engine) -> None:
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
stoploss_order_id, stoploss_last_update,
max_rate, min_rate, sell_reason, strategy,
ticker_interval
ticker_interval, open_trade_price
)
select id, lower(exchange),
case
@ -140,7 +142,8 @@ def check_migrate(engine) -> None:
{initial_stop_loss_pct} initial_stop_loss_pct,
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
{strategy} strategy, {ticker_interval} ticker_interval
{strategy} strategy, {ticker_interval} ticker_interval,
{open_trade_price} open_trade_price
from {table_back_name}
""")
@ -182,6 +185,8 @@ class Trade(_DECL_BASE):
fee_close = Column(Float, nullable=False, default=0.0)
open_rate = Column(Float)
open_rate_requested = Column(Float)
# open_trade_price - calcuated via _calc_open_trade_price
open_trade_price = Column(Float)
close_rate = Column(Float)
close_rate_requested = Column(Float)
close_profit = Column(Float)
@ -210,6 +215,10 @@ class Trade(_DECL_BASE):
strategy = Column(String, nullable=True)
ticker_interval = Column(Integer, nullable=True)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.recalc_open_trade_price()
def __repr__(self):
open_since = self.open_date.strftime('%Y-%m-%d %H:%M:%S') if self.is_open else 'closed'
@ -302,6 +311,7 @@ class Trade(_DECL_BASE):
# Update open rate and actual amount
self.open_rate = Decimal(order['price'])
self.amount = Decimal(order['amount'])
self.recalc_open_trade_price()
logger.info('%s_BUY has been fulfilled for %s.', order_type.upper(), self)
self.open_order_id = None
elif order_type in ('market', 'limit') and order['side'] == 'sell':
@ -322,7 +332,7 @@ class Trade(_DECL_BASE):
and marks trade as closed
"""
self.close_rate = Decimal(rate)
self.close_profit = self.calc_profit_percent()
self.close_profit = self.calc_profit_ratio()
self.close_date = datetime.utcnow()
self.is_open = False
self.open_order_id = None
@ -331,17 +341,22 @@ class Trade(_DECL_BASE):
self
)
def calc_open_trade_price(self, fee: Optional[float] = None) -> float:
def _calc_open_trade_price(self) -> float:
"""
Calculate the open_rate including fee.
:param fee: fee to use on the open rate (optional).
If rate is not set self.fee will be used
Calculate the open_rate including open_fee.
:return: Price in of the open trade incl. Fees
"""
buy_trade = (Decimal(self.amount) * Decimal(self.open_rate))
fees = buy_trade * Decimal(fee or self.fee_open)
buy_trade = Decimal(self.amount) * Decimal(self.open_rate)
fees = buy_trade * Decimal(self.fee_open)
return float(buy_trade + fees)
def recalc_open_trade_price(self) -> None:
"""
Recalculate open_trade_price.
Must be called whenever open_rate or fee_open is changed.
"""
self.open_trade_price = self._calc_open_trade_price()
def calc_close_trade_price(self, rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
"""
@ -355,7 +370,7 @@ class Trade(_DECL_BASE):
if rate is None and not self.close_rate:
return 0.0
sell_trade = (Decimal(self.amount) * Decimal(rate or self.close_rate))
sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate)
fees = sell_trade * Decimal(fee or self.fee_close)
return float(sell_trade - fees)
@ -369,29 +384,27 @@ class Trade(_DECL_BASE):
If rate is not set self.close_rate will be used
:return: profit in stake currency as float
"""
open_trade_price = self.calc_open_trade_price()
close_trade_price = self.calc_close_trade_price(
rate=(rate or self.close_rate),
fee=(fee or self.fee_close)
)
profit = close_trade_price - open_trade_price
profit = close_trade_price - self.open_trade_price
return float(f"{profit:.8f}")
def calc_profit_percent(self, rate: Optional[float] = None,
def calc_profit_ratio(self, rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
"""
Calculates the profit in percentage (including fee).
Calculates the profit as ratio (including fee).
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:param fee: fee to use on the close rate (optional).
:return: profit in percentage as float
:return: profit ratio as float
"""
open_trade_price = self.calc_open_trade_price()
close_trade_price = self.calc_close_trade_price(
rate=(rate or self.close_rate),
fee=(fee or self.fee_close)
)
profit_percent = (close_trade_price / open_trade_price) - 1
profit_percent = (close_trade_price / self.open_trade_price) - 1
return float(f"{profit_percent:.8f}")
@staticmethod

View File

@ -1,6 +1,6 @@
from typing import Any, Dict
from freqtrade import OperationalException
from freqtrade.exceptions import OperationalException
from freqtrade.state import RunMode
from freqtrade.utils import setup_utils_configuration

View File

@ -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']

View File

@ -14,10 +14,10 @@ class ExchangeResolver(IResolver):
"""
This class contains all the logic to load a custom exchange class
"""
object_type = Exchange
__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 +25,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,
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

View File

@ -5,10 +5,10 @@ This module load custom hyperopt
"""
import logging
from pathlib import Path
from typing import Optional, Dict
from typing import Dict
from freqtrade import OperationalException
from freqtrade.constants import DEFAULT_HYPEROPT_LOSS, USERPATH_HYPEROPTS
from freqtrade.exceptions import OperationalException
from freqtrade.optimize.hyperopt_interface import IHyperOpt
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
from freqtrade.resolvers import IResolver
@ -20,11 +20,15 @@ class HyperOptResolver(IResolver):
"""
This class contains all the logic to load custom hyperopt class
"""
__slots__ = ['hyperopt']
object_type = IHyperOpt
object_type_str = "Hyperopt"
user_subdir = USERPATH_HYPEROPTS
initial_search_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
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,50 +37,33 @@ class HyperOptResolver(IResolver):
hyperopt_name = config['hyperopt']
self.hyperopt = self._load_hyperopt(hyperopt_name, config,
hyperopt = HyperOptResolver.load_object(hyperopt_name, config,
kwargs={'config': 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.")
def _load_hyperopt(
self, 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
:param config: configuration dictionary
:param extra_dir: additional directory to search for the given hyperopt
:return: HyperOpt instance or None
"""
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)
hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt,
object_name=hyperopt_name, kwargs={'config': config})
if hyperopt:
return hyperopt
raise OperationalException(
f"Impossible to load Hyperopt '{hyperopt_name}'. This class does not exist "
"or contains Python code errors."
)
class HyperOptLossResolver(IResolver):
"""
This class contains all the logic to load custom hyperopt loss class
"""
__slots__ = ['hyperoptloss']
object_type = IHyperOptLoss
object_type_str = "HyperoptLoss"
user_subdir = USERPATH_HYPEROPTS
initial_search_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
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,38 +73,15 @@ class HyperOptLossResolver(IResolver):
# default hyperopt loss
hyperoptloss_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS
self.hyperoptloss = self._load_hyperoptloss(
hyperoptloss_name, config, extra_dir=config.get('hyperopt_path'))
hyperoptloss = HyperOptLossResolver.load_object(hyperoptloss_name,
config, kwargs={},
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`.")
def _load_hyperoptloss(
self, 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
:param config: configuration dictionary
:param extra_dir: additional directory to search for the given hyperopt
:return: HyperOptLoss instance or None
"""
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)
hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss,
object_name=hyper_loss_name)
if hyperoptloss:
return hyperoptloss
raise OperationalException(
f"Impossible to load HyperoptLoss '{hyper_loss_name}'. This class does not exist "
"or contains Python code errors."
)

View File

@ -7,7 +7,9 @@ import importlib.util
import inspect
import logging
from pathlib import Path
from typing import Any, List, Optional, Tuple, Union, Generator
from typing import Any, Dict, Generator, List, Optional, Tuple, Type, Union
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__)
@ -16,11 +18,17 @@ class IResolver:
"""
This class contains all the logic to load custom classes
"""
# Childclasses need to override this
object_type: Type[Any]
object_type_str: str
user_subdir: Optional[str] = None
initial_search_path: Path
def build_search_paths(self, config, current_path: Path, user_subdir: Optional[str] = None,
@classmethod
def build_search_paths(cls, config, user_subdir: Optional[str] = None,
extra_dir: Optional[str] = None) -> List[Path]:
abs_paths: List[Path] = [current_path]
abs_paths: List[Path] = [cls.initial_search_path]
if user_subdir:
abs_paths.insert(0, config['user_data_dir'].joinpath(user_subdir))
@ -31,12 +39,11 @@ class IResolver:
return abs_paths
@staticmethod
def _get_valid_object(object_type, module_path: Path,
object_name: str) -> Generator[Any, None, None]:
@classmethod
def _get_valid_object(cls, module_path: Path,
object_name: Optional[str]) -> Generator[Any, None, None]:
"""
Generator returning objects with matching object_type and object_name in the path given.
:param object_type: object_type (class)
:param module_path: absolute path to the module
:param object_name: Class name of the object
:return: generator containing matching objects
@ -44,7 +51,7 @@ class IResolver:
# Generate spec based on absolute path
# Pass object_name as first argument to have logging print a reasonable name.
spec = importlib.util.spec_from_file_location(object_name, str(module_path))
spec = importlib.util.spec_from_file_location(object_name or "", str(module_path))
module = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
@ -54,19 +61,20 @@ class IResolver:
valid_objects_gen = (
obj for name, obj in inspect.getmembers(module, inspect.isclass)
if object_name == name and object_type in obj.__bases__
if (object_name is None or object_name == name) and cls.object_type in obj.__bases__
)
return valid_objects_gen
@staticmethod
def _search_object(directory: Path, object_type, object_name: str,
kwargs: dict = {}) -> Union[Tuple[Any, Path], Tuple[None, None]]:
@classmethod
def _search_object(cls, directory: Path, object_name: str
) -> Union[Tuple[Any, Path], Tuple[None, None]]:
"""
Search for the objectname in the given directory
:param directory: relative or absolute directory path
:return: object instance
:param object_name: ClassName of the object to load
:return: object class
"""
logger.debug("Searching for %s %s in '%s'", object_type.__name__, object_name, directory)
logger.debug(f"Searching for {cls.object_type.__name__} {object_name} in '{directory}'")
for entry in directory.iterdir():
# Only consider python files
if not str(entry).endswith('.py'):
@ -74,14 +82,14 @@ class IResolver:
continue
module_path = entry.resolve()
obj = next(IResolver._get_valid_object(object_type, module_path, object_name), None)
obj = next(cls._get_valid_object(module_path, object_name), None)
if obj:
return (obj(**kwargs), module_path)
return (obj, module_path)
return (None, None)
@staticmethod
def _load_object(paths: List[Path], object_type, object_name: str,
@classmethod
def _load_object(cls, paths: List[Path], object_name: str,
kwargs: dict = {}) -> Optional[Any]:
"""
Try to load object from path list.
@ -89,16 +97,63 @@ class IResolver:
for _path in paths:
try:
(module, module_path) = IResolver._search_object(directory=_path,
object_type=object_type,
object_name=object_name,
kwargs=kwargs)
(module, module_path) = cls._search_object(directory=_path,
object_name=object_name)
if module:
logger.info(
f"Using resolved {object_type.__name__.lower()[1:]} {object_name} "
f"Using resolved {cls.object_type.__name__.lower()[1:]} {object_name} "
f"from '{module_path}'...")
return module
return module(**kwargs)
except FileNotFoundError:
logger.warning('Path "%s" does not exist.', _path.resolve())
return None
@classmethod
def load_object(cls, object_name: str, config: dict, kwargs: dict,
extra_dir: Optional[str] = None) -> Any:
"""
Search and loads the specified object as configured in hte child class.
:param objectname: name of the module to import
:param config: configuration dictionary
:param extra_dir: additional directory to search for the given pairlist
:raises: OperationalException if the class is invalid or does not exist.
:return: Object instance or None
"""
abs_paths = cls.build_search_paths(config,
user_subdir=cls.user_subdir,
extra_dir=extra_dir)
pairlist = cls._load_object(paths=abs_paths, object_name=object_name,
kwargs=kwargs)
if pairlist:
return pairlist
raise OperationalException(
f"Impossible to load {cls.object_type_str} '{object_name}'. This class does not exist "
"or contains Python code errors."
)
@classmethod
def search_all_objects(cls, directory: Path) -> List[Dict[str, Any]]:
"""
Searches a directory for valid objects
:param directory: Path to search
:return: List of dicts containing 'name', 'class' and 'location' entires
"""
logger.debug(f"Searching for {cls.object_type.__name__} '{directory}'")
objects = []
for entry in directory.iterdir():
# Only consider python files
if not str(entry).endswith('.py'):
logger.debug('Ignoring %s', entry)
continue
module_path = entry.resolve()
logger.debug(f"Path {module_path}")
for obj in cls._get_valid_object(module_path, object_name=None):
objects.append(
{'name': obj.__name__,
'class': obj,
'location': entry,
})
return objects

View File

@ -6,7 +6,6 @@ This module load custom pairlists
import logging
from pathlib import Path
from freqtrade import OperationalException
from freqtrade.pairlist.IPairList import IPairList
from freqtrade.resolvers import IResolver
@ -17,41 +16,28 @@ class PairListResolver(IResolver):
"""
This class contains all the logic to load custom PairList class
"""
object_type = IPairList
object_type_str = "Pairlist"
user_subdir = None
initial_search_path = Path(__file__).parent.parent.joinpath('pairlist').resolve()
__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,
return PairListResolver.load_object(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:
"""
Search and loads the specified pairlist.
:param pairlist_name: name of the module to import
:param config: configuration dictionary
:param extra_dir: additional directory to search for the given pairlist
:return: PairList instance or None
"""
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)
pairlist = self._load_object(paths=abs_paths, object_type=IPairList,
object_name=pairlist_name, kwargs=kwargs)
if pairlist:
return pairlist
raise OperationalException(
f"Impossible to load Pairlist '{pairlist_name}'. This class does not exist "
"or contains Python code errors."
'pairlist_pos': pairlist_pos},
)

View File

@ -11,7 +11,9 @@ from inspect import getfullargspec
from pathlib import Path
from typing import Dict, Optional
from freqtrade import constants, OperationalException
from freqtrade.constants import (REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES,
USERPATH_STRATEGY)
from freqtrade.exceptions import OperationalException
from freqtrade.resolvers import IResolver
from freqtrade.strategy.interface import IStrategy
@ -20,12 +22,15 @@ 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
"""
object_type = IStrategy
object_type_str = "Strategy"
user_subdir = USERPATH_STRATEGY
initial_search_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
__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,8 +42,8 @@ class StrategyResolver(IResolver):
"the strategy class to use.")
strategy_name = config['strategy']
self.strategy: IStrategy = self._load_strategy(strategy_name,
config=config,
strategy: IStrategy = StrategyResolver._load_strategy(
strategy_name, config=config,
extra_dir=config.get('strategy_path'))
# make sure ask_strategy dict is available
@ -61,15 +66,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 +87,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 +105,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 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 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
@ -126,10 +138,9 @@ class StrategyResolver(IResolver):
:param extra_dir: additional directory to search for the given strategy
:return: Strategy instance or None
"""
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,
abs_paths = StrategyResolver.build_search_paths(config,
user_subdir=USERPATH_STRATEGY,
extra_dir=extra_dir)
if ":" in strategy_name:
@ -148,8 +159,9 @@ 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 = StrategyResolver._load_object(paths=abs_paths,
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)

View File

@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple
import arrow
from numpy import NAN, mean
from freqtrade import DependencyException, TemporaryError
from freqtrade.exceptions import DependencyException, TemporaryError
from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
@ -123,7 +123,7 @@ class RPC:
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
except DependencyException:
current_rate = NAN
current_profit = trade.calc_profit_percent(current_rate)
current_profit = trade.calc_profit_ratio(current_rate)
fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
if trade.close_profit else None)
trade_dict = trade.to_json()
@ -142,7 +142,7 @@ class RPC:
def _rpc_status_table(self, stake_currency, fiat_display_currency: str) -> Tuple[List, List]:
trades = Trade.get_open_trades()
if not trades:
raise RPCException('no active order')
raise RPCException('no active trade')
else:
trades_list = []
for trade in trades:
@ -151,7 +151,7 @@ class RPC:
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
except DependencyException:
current_rate = NAN
trade_perc = (100 * trade.calc_profit_percent(current_rate))
trade_perc = (100 * trade.calc_profit_ratio(current_rate))
trade_profit = trade.calc_profit(current_rate)
profit_str = f'{trade_perc:.2f}%'
if self._fiat_converter:
@ -240,7 +240,7 @@ class RPC:
durations.append((trade.close_date - trade.open_date).total_seconds())
if not trade.is_open:
profit_percent = trade.calc_profit_percent()
profit_percent = trade.calc_profit_ratio()
profit_closed_coin.append(trade.calc_profit())
profit_closed_perc.append(profit_percent)
else:
@ -249,7 +249,7 @@ class RPC:
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
except DependencyException:
current_rate = NAN
profit_percent = trade.calc_profit_percent(rate=current_rate)
profit_percent = trade.calc_profit_ratio(rate=current_rate)
profit_all_coin.append(
trade.calc_profit(rate=trade.close_rate or current_rate)
@ -341,13 +341,15 @@ 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 ''
}
def _rpc_start(self) -> Dict[str, str]:
@ -460,7 +462,7 @@ class RPC:
raise RPCException(f'position for {pair} already open - id: {trade.id}')
# gen stake amount
stakeamount = self._freqtrade._get_trade_stake_amount(pair)
stakeamount = self._freqtrade.get_trade_stake_amount(pair)
# execute buy
if self._freqtrade.execute_buy(pair, stakeamount, price):

View File

@ -331,7 +331,15 @@ class Telegram(RPC):
try:
result = self._rpc_balance(self._config['stake_currency'],
self._config.get('fiat_display_currency', ''))
output = ''
if self._config['dry_run']:
output += (
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"
)
for currency in result['currencies']:
if currency['est_stake'] > 0.0001:
curr_output = "*{currency}:*\n" \
@ -350,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:

View File

@ -168,12 +168,25 @@ 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)`
"""
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:
"""
Checks if a pair is currently locked
@ -302,7 +315,7 @@ class IStrategy(ABC):
"""
# Set current rate to low for backtesting sell
current_rate = low or rate
current_profit = trade.calc_profit_percent(current_rate)
current_profit = trade.calc_profit_ratio(current_rate)
trade.adjust_min_max_rates(high or current_rate)
@ -317,7 +330,7 @@ class IStrategy(ABC):
# Set current rate to high for backtesting sell
current_rate = high or rate
current_profit = trade.calc_profit_percent(current_rate)
current_profit = trade.calc_profit_ratio(current_rate)
config_ask_strategy = self.config.get('ask_strategy', {})
if buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False):
@ -366,7 +379,7 @@ class IStrategy(ABC):
sl_offset = self.trailing_stop_positive_offset
# Make sure current_profit is calculated using high for backtesting.
high_profit = current_profit if not high else trade.calc_profit_percent(high)
high_profit = current_profit if not high else trade.calc_profit_ratio(high)
# Don't update stoploss if trailing_only_offset_is_reached is true.
if not (self.trailing_only_offset_is_reached and high_profit < sl_offset):

View File

@ -47,6 +47,7 @@ class {{ strategy }}(IStrategy):
# Trailing stoploss
trailing_stop = False
# trailing_only_offset_is_reached = False
# trailing_stop_positive = 0.01
# trailing_stop_positive_offset = 0.0 # Disabled / not configured

View File

@ -48,6 +48,7 @@ class SampleStrategy(IStrategy):
# Trailing stoploss
trailing_stop = False
# trailing_only_offset_is_reached = False
# trailing_stop_positive = 0.01
# trailing_stop_positive_offset = 0.0 # Disabled / not configured

View File

@ -73,9 +73,9 @@
"source": [
"# Load strategy using values set above\n",
"from freqtrade.resolvers import StrategyResolver\n",
"strategy = StrategyResolver({'strategy': strategy_name,\n",
"strategy = StrategyResolver.load_strategy({'strategy': strategy_name,\n",
" 'user_data_dir': user_data_dir,\n",
" 'strategy_path': strategy_location}).strategy\n",
" 'strategy_path': strategy_location})\n",
"\n",
"# Generate buy/sell signals using strategy\n",
"df = strategy.analyze_ticker(candles, {'pair': pair})\n",

View File

@ -11,7 +11,6 @@ import rapidjson
from colorama import init as colorama_init
from tabulate import tabulate
from freqtrade import OperationalException
from freqtrade.configuration import (Configuration, TimeRange,
remove_credentials)
from freqtrade.configuration.directory_operations import (copy_sample_files,
@ -20,10 +19,11 @@ from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGY
from freqtrade.data.history import (convert_trades_to_ohlcv,
refresh_backtest_ohlcv_data,
refresh_backtest_trades_data)
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import (available_exchanges, ccxt_exchanges,
market_is_active, symbol_is_pair)
from freqtrade.misc import plural, render_template
from freqtrade.resolvers import ExchangeResolver
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.state import RunMode
logger = logging.getLogger(__name__)
@ -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"],
dl_path=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 ...")
@ -224,6 +223,24 @@ def start_download_data(args: Dict[str, Any]) -> None:
f"on exchange {exchange.name}.")
def start_list_strategies(args: Dict[str, Any]) -> None:
"""
Print Strategies available in a directory
"""
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGY))
strategies = StrategyResolver.search_all_objects(directory)
# Sort alphabetically
strategies = sorted(strategies, key=lambda x: x['name'])
strats_to_print = [{'name': s['name'], 'location': s['location'].name} for s in strategies]
if args['print_one_column']:
print('\n'.join([s['name'] for s in strategies]))
else:
print(tabulate(strats_to_print, headers='keys', tablefmt='pipe'))
def start_list_timeframes(args: Dict[str, Any]) -> None:
"""
Print ticker intervals (timeframes) available on Exchange
@ -233,7 +250,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 +269,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 +350,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:

View File

@ -4,7 +4,7 @@
import logging
from typing import Dict, NamedTuple, Any
from freqtrade.exchange import Exchange
from freqtrade import constants
from freqtrade.persistence import Trade
logger = logging.getLogger(__name__)
@ -23,14 +23,12 @@ class Wallets:
self._config = config
self._exchange = exchange
self._wallets: Dict[str, Wallet] = {}
self.start_cap = config['dry_run_wallet']
self.update()
def get_free(self, currency) -> float:
if self._config['dry_run']:
return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET)
balance = self._wallets.get(currency)
if balance and balance.free:
return balance.free
@ -39,9 +37,6 @@ class Wallets:
def get_used(self, currency) -> float:
if self._config['dry_run']:
return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET)
balance = self._wallets.get(currency)
if balance and balance.used:
return balance.used
@ -50,16 +45,45 @@ class Wallets:
def get_total(self, currency) -> float:
if self._config['dry_run']:
return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET)
balance = self._wallets.get(currency)
if balance and balance.total:
return balance.total
else:
return 0
def update(self) -> None:
def _update_dry(self) -> None:
"""
Update from database in dry-run mode
- Apply apply profits of closed trades on top of stake amount
- 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
_wallets[self._config['stake_currency']] = Wallet(
self._config['stake_currency'],
current_stake,
0,
current_stake
)
for trade in open_trades:
curr = trade.pair.split('/')[0]
_wallets[curr] = Wallet(
curr,
trade.amount,
0,
trade.amount
)
self._wallets = _wallets
def _update_live(self) -> None:
balances = self._exchange.get_balances()
@ -71,6 +95,11 @@ class Wallets:
balances[currency].get('total', None)
)
def update(self) -> None:
if self._config['dry_run']:
self._update_dry()
else:
self._update_live()
logger.info('Wallets synced.')
def get_all_balances(self) -> Dict[str, Any]:

View File

@ -8,9 +8,9 @@ from typing import Any, Callable, Dict, Optional
import sdnotify
from freqtrade import (OperationalException, TemporaryError, __version__,
constants)
from freqtrade import __version__, constants
from freqtrade.configuration import Configuration
from freqtrade.exceptions import OperationalException, TemporaryError
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.rpc import RPCMessageType
from freqtrade.state import State

View File

@ -1,10 +1,10 @@
# requirements without requirements installable via conda
# mainly used for Raspberry pi installs
ccxt==1.20.46
SQLAlchemy==1.3.11
ccxt==1.21.23
SQLAlchemy==1.3.12
python-telegram-bot==12.2.0
arrow==0.15.4
cachetools==3.1.1
cachetools==4.0.0
requests==2.22.0
urllib3==1.25.7
wrapt==1.11.2

View File

@ -7,8 +7,8 @@ coveralls==1.9.2
flake8==3.7.9
flake8-type-annotations==0.1.0
flake8-tidy-imports==3.1.0
mypy==0.750
pytest==5.3.1
mypy==0.761
pytest==5.3.2
pytest-asyncio==0.10.0
pytest-cov==2.8.1
pytest-mock==1.13.0

View File

@ -2,8 +2,8 @@
-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
joblib==0.14.0
joblib==0.14.1

View File

@ -1,5 +1,5 @@
# Include all requirements to run the bot.
-r requirements.txt
plotly==4.3.0
plotly==4.4.1

View File

@ -1,5 +1,5 @@
# Load common requirements
-r requirements-common.txt
numpy==1.17.4
numpy==1.18.0
pandas==0.25.3

View File

@ -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',
])

View File

@ -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

View File

@ -2,7 +2,7 @@
import logging
from freqtrade.data.converter import parse_ticker_dataframe, ohlcv_fill_up_missing_data
from freqtrade.data.history import load_pair_history, validate_backtest_data, get_timeframe
from freqtrade.data.history import load_pair_history, validate_backtest_data, get_timerange
from tests.conftest import log_has
@ -36,7 +36,7 @@ def test_ohlcv_fill_up_missing_data(testdatadir, caplog):
f"{len(data)} - after: {len(data2)}", caplog)
# Test fillup actually fixes invalid backtest data
min_date, max_date = get_timeframe({'UNITTEST/BTC': data})
min_date, max_date = get_timerange({'UNITTEST/BTC': data})
assert validate_backtest_data(data, 'UNITTEST/BTC', min_date, max_date, 1)
assert not validate_backtest_data(data2, 'UNITTEST/BTC', min_date, max_date, 1)

View File

@ -7,21 +7,21 @@ from shutil import copyfile
from unittest.mock import MagicMock, PropertyMock
import arrow
import pytest
from pandas import DataFrame
from freqtrade import OperationalException
from freqtrade.configuration import TimeRange
from freqtrade.data import history
from freqtrade.data.history import (_load_cached_data_for_updating,
convert_trades_to_ohlcv,
download_pair_history,
download_trades_history,
from freqtrade.data.history import (_download_pair_history,
_download_trades_history,
_load_cached_data_for_updating,
convert_trades_to_ohlcv, get_timerange,
load_data, load_pair_history,
load_tickerdata_file, pair_data_filename,
pair_trades_filename,
refresh_backtest_ohlcv_data,
refresh_backtest_trades_data,
trim_tickerlist)
refresh_data,
trim_dataframe, trim_tickerlist,
validate_backtest_data)
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import file_dump_json
from freqtrade.strategy.default_strategy import DefaultStrategy
@ -64,7 +64,7 @@ def _clean_test_file(file: Path) -> None:
def test_load_data_30min_ticker(mocker, caplog, default_conf, testdatadir) -> None:
ld = history.load_pair_history(pair='UNITTEST/BTC', timeframe='30m', datadir=testdatadir)
ld = load_pair_history(pair='UNITTEST/BTC', timeframe='30m', datadir=testdatadir)
assert isinstance(ld, DataFrame)
assert not log_has(
'Download history data for pair: "UNITTEST/BTC", timeframe: 30m '
@ -73,7 +73,7 @@ def test_load_data_30min_ticker(mocker, caplog, default_conf, testdatadir) -> No
def test_load_data_7min_ticker(mocker, caplog, default_conf, testdatadir) -> None:
ld = history.load_pair_history(pair='UNITTEST/BTC', timeframe='7m', datadir=testdatadir)
ld = load_pair_history(pair='UNITTEST/BTC', timeframe='7m', datadir=testdatadir)
assert isinstance(ld, DataFrame)
assert ld.empty
assert log_has(
@ -86,7 +86,7 @@ def test_load_data_1min_ticker(ticker_history, mocker, caplog, testdatadir) -> N
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history)
file = testdatadir / 'UNITTEST_BTC-1m.json'
_backup_file(file, copy_file=True)
history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'])
load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'])
assert file.is_file()
assert not log_has(
'Download history data for pair: "UNITTEST/BTC", interval: 1m '
@ -99,10 +99,9 @@ def test_load_data_startup_candles(mocker, caplog, default_conf, testdatadir) ->
ltfmock = mocker.patch('freqtrade.data.history.load_tickerdata_file',
MagicMock(return_value=None))
timerange = TimeRange('date', None, 1510639620, 0)
history.load_pair_history(pair='UNITTEST/BTC', timeframe='1m',
load_pair_history(pair='UNITTEST/BTC', timeframe='1m',
datadir=testdatadir, timerange=timerange,
startup_candles=20,
)
startup_candles=20,)
assert ltfmock.call_count == 1
assert ltfmock.call_args_list[0][1]['timerange'] != timerange
@ -121,9 +120,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog,
_backup_file(file)
# do not download a new pair if refresh_pairs isn't set
history.load_pair_history(datadir=testdatadir,
timeframe='1m',
pair='MEME/BTC')
load_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC')
assert not file.is_file()
assert log_has(
'No history data for pair: "MEME/BTC", timeframe: 1m. '
@ -131,22 +128,14 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog,
)
# download a new pair if refresh_pairs is set
history.load_pair_history(datadir=testdatadir,
timeframe='1m',
refresh_pairs=True,
exchange=exchange,
pair='MEME/BTC')
refresh_data(datadir=testdatadir, timeframe='1m', pairs=['MEME/BTC'],
exchange=exchange)
load_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC')
assert file.is_file()
assert log_has_re(
'Download history data for pair: "MEME/BTC", timeframe: 1m '
'and store in .*', caplog
)
with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'):
history.load_pair_history(datadir=testdatadir,
timeframe='1m',
refresh_pairs=True,
exchange=None,
pair='MEME/BTC')
_clean_test_file(file)
@ -267,10 +256,10 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf, testda
assert not file1_1.is_file()
assert not file2_1.is_file()
assert download_pair_history(datadir=testdatadir, exchange=exchange,
assert _download_pair_history(datadir=testdatadir, exchange=exchange,
pair='MEME/BTC',
timeframe='1m')
assert download_pair_history(datadir=testdatadir, exchange=exchange,
assert _download_pair_history(datadir=testdatadir, exchange=exchange,
pair='CFI/BTC',
timeframe='1m')
assert not exchange._pairs_last_refresh_time
@ -284,10 +273,10 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf, testda
assert not file1_5.is_file()
assert not file2_5.is_file()
assert download_pair_history(datadir=testdatadir, exchange=exchange,
assert _download_pair_history(datadir=testdatadir, exchange=exchange,
pair='MEME/BTC',
timeframe='5m')
assert download_pair_history(datadir=testdatadir, exchange=exchange,
assert _download_pair_history(datadir=testdatadir, exchange=exchange,
pair='CFI/BTC',
timeframe='5m')
assert not exchange._pairs_last_refresh_time
@ -307,8 +296,8 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None:
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick)
exchange = get_patched_exchange(mocker, default_conf)
download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='1m')
download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='3m')
_download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='1m')
_download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='3m')
assert json_dump_mock.call_count == 2
@ -324,7 +313,7 @@ def test_download_backtesting_data_exception(ticker_history, mocker, caplog,
_backup_file(file1_1)
_backup_file(file1_5)
assert not download_pair_history(datadir=testdatadir, exchange=exchange,
assert not _download_pair_history(datadir=testdatadir, exchange=exchange,
pair='MEME/BTC',
timeframe='1m')
# clean files freshly downloaded
@ -351,10 +340,8 @@ def test_load_partial_missing(testdatadir, caplog) -> None:
# Make sure we start fresh - test missing data at start
start = arrow.get('2018-01-01T00:00:00')
end = arrow.get('2018-01-11T00:00:00')
tickerdata = history.load_data(testdatadir, '5m', ['UNITTEST/BTC'],
startup_candles=20,
timerange=TimeRange('date', 'date',
start.timestamp, end.timestamp))
tickerdata = load_data(testdatadir, '5m', ['UNITTEST/BTC'], startup_candles=20,
timerange=TimeRange('date', 'date', start.timestamp, end.timestamp))
assert log_has(
'Using indicator startup period: 20 ...', caplog
)
@ -369,10 +356,8 @@ def test_load_partial_missing(testdatadir, caplog) -> None:
caplog.clear()
start = arrow.get('2018-01-10T00:00:00')
end = arrow.get('2018-02-20T00:00:00')
tickerdata = history.load_data(datadir=testdatadir, timeframe='5m',
pairs=['UNITTEST/BTC'],
timerange=TimeRange('date', 'date',
start.timestamp, end.timestamp))
tickerdata = load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
timerange=TimeRange('date', 'date', start.timestamp, end.timestamp))
# timedifference in 5 minutes
td = ((end - start).total_seconds() // 60 // 5) + 1
assert td != len(tickerdata['UNITTEST/BTC'])
@ -384,12 +369,24 @@ def test_load_partial_missing(testdatadir, caplog) -> None:
def test_init(default_conf, mocker) -> None:
exchange = get_patched_exchange(mocker, default_conf)
assert {} == history.load_data(
assert {} == load_data(
datadir='',
pairs=[],
timeframe=default_conf['ticker_interval']
)
def test_init_with_refresh(default_conf, mocker) -> None:
exchange = get_patched_exchange(mocker, default_conf)
refresh_data(
datadir='',
pairs=[],
timeframe=default_conf['ticker_interval'],
exchange=exchange
)
assert {} == load_data(
datadir='',
exchange=exchange,
pairs=[],
refresh_pairs=True,
timeframe=default_conf['ticker_interval']
)
@ -447,7 +444,7 @@ def test_trim_tickerlist(testdatadir) -> None:
def test_trim_dataframe(testdatadir) -> None:
data = history.load_data(
data = load_data(
datadir=testdatadir,
timeframe='1m',
pairs=['UNITTEST/BTC']
@ -458,7 +455,7 @@ def test_trim_dataframe(testdatadir) -> None:
# Remove first 30 minutes (1800 s)
tr = TimeRange('date', None, min_date + 1800, 0)
data_modify = history.trim_dataframe(data_modify, tr)
data_modify = trim_dataframe(data_modify, tr)
assert not data_modify.equals(data)
assert len(data_modify) < len(data)
assert len(data_modify) == len(data) - 30
@ -468,7 +465,7 @@ def test_trim_dataframe(testdatadir) -> None:
data_modify = data.copy()
# Remove last 30 minutes (1800 s)
tr = TimeRange(None, 'date', 0, max_date - 1800)
data_modify = history.trim_dataframe(data_modify, tr)
data_modify = trim_dataframe(data_modify, tr)
assert not data_modify.equals(data)
assert len(data_modify) < len(data)
assert len(data_modify) == len(data) - 30
@ -478,7 +475,7 @@ def test_trim_dataframe(testdatadir) -> None:
data_modify = data.copy()
# Remove first 25 and last 30 minutes (1800 s)
tr = TimeRange('date', 'date', min_date + 1500, max_date - 1800)
data_modify = history.trim_dataframe(data_modify, tr)
data_modify = trim_dataframe(data_modify, tr)
assert not data_modify.equals(data)
assert len(data_modify) < len(data)
assert len(data_modify) == len(data) - 55
@ -510,18 +507,18 @@ def test_file_dump_json_tofile(testdatadir) -> None:
_clean_test_file(file)
def test_get_timeframe(default_conf, mocker, testdatadir) -> None:
def test_get_timerange(default_conf, mocker, testdatadir) -> None:
patch_exchange(mocker)
strategy = DefaultStrategy(default_conf)
data = strategy.tickerdata_to_dataframe(
history.load_data(
load_data(
datadir=testdatadir,
timeframe='1m',
pairs=['UNITTEST/BTC']
)
)
min_date, max_date = history.get_timeframe(data)
min_date, max_date = get_timerange(data)
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
assert max_date.isoformat() == '2017-11-14T22:58:00+00:00'
@ -531,16 +528,16 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir)
strategy = DefaultStrategy(default_conf)
data = strategy.tickerdata_to_dataframe(
history.load_data(
load_data(
datadir=testdatadir,
timeframe='1m',
pairs=['UNITTEST/BTC'],
fill_up_missing=False
)
)
min_date, max_date = history.get_timeframe(data)
min_date, max_date = get_timerange(data)
caplog.clear()
assert history.validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC',
assert validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC',
min_date, max_date, timeframe_to_minutes('1m'))
assert len(caplog.record_tuples) == 1
assert log_has(
@ -554,7 +551,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No
timerange = TimeRange('index', 'index', 200, 250)
data = strategy.tickerdata_to_dataframe(
history.load_data(
load_data(
datadir=testdatadir,
timeframe='5m',
pairs=['UNITTEST/BTC'],
@ -562,15 +559,15 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No
)
)
min_date, max_date = history.get_timeframe(data)
min_date, max_date = get_timerange(data)
caplog.clear()
assert not history.validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC',
assert not validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC',
min_date, max_date, timeframe_to_minutes('5m'))
assert len(caplog.record_tuples) == 0
def test_refresh_backtest_ohlcv_data(mocker, default_conf, markets, caplog, testdatadir):
dl_mock = mocker.patch('freqtrade.data.history.download_pair_history', MagicMock())
dl_mock = mocker.patch('freqtrade.data.history._download_pair_history', MagicMock())
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
)
@ -580,7 +577,7 @@ def test_refresh_backtest_ohlcv_data(mocker, default_conf, markets, caplog, test
ex = get_patched_exchange(mocker, default_conf)
timerange = TimeRange.parse_timerange("20190101-20190102")
refresh_backtest_ohlcv_data(exchange=ex, pairs=["ETH/BTC", "XRP/BTC"],
timeframes=["1m", "5m"], dl_path=testdatadir,
timeframes=["1m", "5m"], datadir=testdatadir,
timerange=timerange, erase=True
)
@ -591,7 +588,7 @@ def test_refresh_backtest_ohlcv_data(mocker, default_conf, markets, caplog, test
def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir):
dl_mock = mocker.patch('freqtrade.data.history.download_pair_history', MagicMock())
dl_mock = mocker.patch('freqtrade.data.history._download_pair_history', MagicMock())
ex = get_patched_exchange(mocker, default_conf)
mocker.patch(
@ -600,7 +597,7 @@ def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir):
timerange = TimeRange.parse_timerange("20190101-20190102")
unav_pairs = refresh_backtest_ohlcv_data(exchange=ex, pairs=["BTT/BTC", "LTC/USDT"],
timeframes=["1m", "5m"],
dl_path=testdatadir,
datadir=testdatadir,
timerange=timerange, erase=False
)
@ -611,7 +608,7 @@ def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir):
def test_refresh_backtest_trades_data(mocker, default_conf, markets, caplog, testdatadir):
dl_mock = mocker.patch('freqtrade.data.history.download_trades_history', MagicMock())
dl_mock = mocker.patch('freqtrade.data.history._download_trades_history', MagicMock())
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
)
@ -646,7 +643,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad
assert not file1.is_file()
assert download_trades_history(datadir=testdatadir, exchange=exchange,
assert _download_trades_history(datadir=testdatadir, exchange=exchange,
pair='ETH/BTC')
assert log_has("New Amount of trades: 5", caplog)
assert file1.is_file()
@ -657,7 +654,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad
mocker.patch('freqtrade.exchange.Exchange.get_historic_trades',
MagicMock(side_effect=ValueError))
assert not download_trades_history(datadir=testdatadir, exchange=exchange,
assert not _download_trades_history(datadir=testdatadir, exchange=exchange,
pair='ETH/BTC')
assert log_has_re('Failed to download historic trades for pair: "ETH/BTC".*', caplog)
@ -668,12 +665,8 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog):
file1 = testdatadir / 'XRP_ETH-1m.json'
file5 = testdatadir / 'XRP_ETH-5m.json'
# Compare downloaded dataset with converted dataset
dfbak_1m = history.load_pair_history(datadir=testdatadir,
timeframe="1m",
pair=pair)
dfbak_5m = history.load_pair_history(datadir=testdatadir,
timeframe="5m",
pair=pair)
dfbak_1m = load_pair_history(datadir=testdatadir, timeframe="1m", pair=pair)
dfbak_5m = load_pair_history(datadir=testdatadir, timeframe="5m", pair=pair)
_backup_file(file1, copy_file=True)
_backup_file(file5)
@ -685,12 +678,8 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog):
assert log_has("Deleting existing data for pair XRP/ETH, interval 1m.", caplog)
# Load new data
df_1m = history.load_pair_history(datadir=testdatadir,
timeframe="1m",
pair=pair)
df_5m = history.load_pair_history(datadir=testdatadir,
timeframe="5m",
pair=pair)
df_1m = load_pair_history(datadir=testdatadir, timeframe="1m", pair=pair)
df_5m = load_pair_history(datadir=testdatadir, timeframe="5m", pair=pair)
assert df_1m.equals(dfbak_1m)
assert df_5m.equals(dfbak_5m)

View File

@ -10,7 +10,7 @@ import numpy as np
import pytest
from pandas import DataFrame, to_datetime
from freqtrade import OperationalException
from freqtrade.exceptions import OperationalException
from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.edge import Edge, PairInfo
from freqtrade.strategy.interface import SellType
@ -255,8 +255,8 @@ def test_edge_heartbeat_calculate(mocker, edge_conf):
assert edge.calculate() is False
def mocked_load_data(datadir, pairs=[], timeframe='0m', refresh_pairs=False,
timerange=None, exchange=None, *args, **kwargs):
def mocked_load_data(datadir, pairs=[], timeframe='0m',
timerange=None, *args, **kwargs):
hz = 0.1
base = 0.001
@ -290,6 +290,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', refresh_pairs=False,
def test_edge_process_downloaded_data(mocker, edge_conf):
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
mocker.patch('freqtrade.data.history.refresh_data', MagicMock())
mocker.patch('freqtrade.data.history.load_data', mocked_load_data)
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
@ -301,6 +302,7 @@ def test_edge_process_downloaded_data(mocker, edge_conf):
def test_edge_process_no_data(mocker, edge_conf, caplog):
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
mocker.patch('freqtrade.data.history.refresh_data', MagicMock())
mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={}))
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
@ -313,6 +315,7 @@ def test_edge_process_no_data(mocker, edge_conf, caplog):
def test_edge_process_no_trades(mocker, edge_conf, caplog):
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
mocker.patch('freqtrade.data.history.refresh_data', MagicMock())
mocker.patch('freqtrade.data.history.load_data', mocked_load_data)
# Return empty
mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[]))

View File

@ -4,7 +4,7 @@ from unittest.mock import MagicMock
import ccxt
import pytest
from freqtrade import (DependencyException, InvalidOrderException,
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError)
from tests.conftest import get_patched_exchange

View File

@ -11,7 +11,7 @@ import ccxt
import pytest
from pandas import DataFrame
from freqtrade import (DependencyException, InvalidOrderException,
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.exchange import Binance, Exchange, Kraken
from freqtrade.exchange.common import API_RETRY_COUNT
@ -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())
@ -876,6 +877,7 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name):
def test_get_balance_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
default_conf['dry_run_wallet'] = 999.9
exchange = get_patched_exchange(mocker, default_conf)
assert exchange.get_balance(currency='BTC') == 999.9
@ -976,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',
@ -988,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
@ -1005,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
@ -1017,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)

View File

@ -4,7 +4,7 @@ from unittest.mock import MagicMock
import pytest
from freqtrade.data.history import get_timeframe
from freqtrade.data.history import get_timerange
from freqtrade.optimize.backtesting import Backtesting
from freqtrade.strategy.interface import SellType
from tests.conftest import patch_exchange
@ -380,7 +380,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
pair = "UNITTEST/BTC"
# Dummy data as we mock the analyze functions
data_processed = {pair: frame.copy()}
min_date, max_date = get_timeframe({pair: frame})
min_date, max_date = get_timerange({pair: frame})
results = backtesting.backtest(
processed=data_processed,
stake_amount=default_conf['stake_amount'],

View File

@ -10,13 +10,14 @@ import pandas as pd
import pytest
from arrow import Arrow
from freqtrade import DependencyException, OperationalException, constants
from freqtrade import constants
from freqtrade.configuration import TimeRange
from freqtrade.data import history
from freqtrade.data.btanalysis import evaluate_result_multi
from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.data.dataprovider import DataProvider
from freqtrade.data.history import get_timeframe
from freqtrade.data.history import get_timerange
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.optimize import setup_configuration, start_backtesting
from freqtrade.optimize.backtesting import Backtesting
from freqtrade.state import RunMode
@ -25,7 +26,6 @@ from freqtrade.strategy.interface import SellType
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
patched_configuration_load_config_file)
ORDER_TYPES = [
{
'buy': 'limit',
@ -100,7 +100,7 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None:
data = load_data_test(contour, testdatadir)
processed = backtesting.strategy.tickerdata_to_dataframe(data)
min_date, max_date = get_timeframe(processed)
min_date, max_date = get_timerange(processed)
assert isinstance(processed, dict)
results = backtesting.backtest(
processed=processed,
@ -114,8 +114,8 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None:
assert len(results) == num_results
def mocked_load_data(datadir, pairs=[], timeframe='0m', refresh_pairs=False,
timerange=None, exchange=None, live=False, *args, **kwargs):
def mocked_load_data(datadir, pairs=[], timeframe='0m',
timerange=None, *args, **kwargs):
tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange)
pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', pair="UNITTEST/BTC",
fill_missing=True)}
@ -136,7 +136,7 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
patch_exchange(mocker)
backtesting = Backtesting(conf)
processed = backtesting.strategy.tickerdata_to_dataframe(data)
min_date, max_date = get_timeframe(processed)
min_date, max_date = get_timerange(processed)
return {
'processed': processed,
'stake_amount': conf['stake_amount'],
@ -391,8 +391,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],
@ -401,10 +401,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
@ -455,11 +455,11 @@ def test_generate_text_table_strategyn(default_conf, mocker):
def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
def get_timeframe(input1):
def get_timerange(input1):
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
mocker.patch('freqtrade.data.history.load_data', mocked_load_data)
mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe)
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
patch_exchange(mocker)
mocker.patch.multiple(
@ -488,11 +488,11 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None:
def get_timeframe(input1):
def get_timerange(input1):
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=pd.DataFrame()))
mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe)
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
patch_exchange(mocker)
mocker.patch.multiple(
@ -522,7 +522,7 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None:
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
timerange=timerange)
data_processed = backtesting.strategy.tickerdata_to_dataframe(data)
min_date, max_date = get_timeframe(data_processed)
min_date, max_date = get_timerange(data_processed)
results = backtesting.backtest(
processed=data_processed,
stake_amount=default_conf['stake_amount'],
@ -576,7 +576,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker, testdatadir) -
data = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'],
timerange=timerange)
processed = backtesting.strategy.tickerdata_to_dataframe(data)
min_date, max_date = get_timeframe(processed)
min_date, max_date = get_timerange(processed)
results = backtesting.backtest(
processed=processed,
stake_amount=default_conf['stake_amount'],
@ -694,7 +694,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
backtesting.strategy.advise_sell = _trend_alternate_hold # Override
data_processed = backtesting.strategy.tickerdata_to_dataframe(data)
min_date, max_date = get_timeframe(data_processed)
min_date, max_date = get_timerange(data_processed)
backtest_conf = {
'processed': data_processed,
'stake_amount': default_conf['stake_amount'],

View File

@ -9,7 +9,7 @@ import pytest
from arrow import Arrow
from filelock import Timeout
from freqtrade import OperationalException
from freqtrade.exceptions import OperationalException
from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.data.history import load_tickerdata_file
from freqtrade.optimize import setup_configuration, start_hyperopt
@ -159,11 +159,11 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
delattr(hyperopt, 'populate_buy_trend')
delattr(hyperopt, 'populate_sell_trend')
mocker.patch(
'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver._load_hyperopt',
'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver.load_object',
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,17 +188,17 @@ 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:
hl = DefaultHyperOptLoss
mocker.patch(
'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss',
'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object',
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:
@ -251,7 +251,7 @@ def test_start_no_data(mocker, default_conf, caplog) -> None:
patched_configuration_load_config_file(mocker, default_conf)
mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=pd.DataFrame))
mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe',
'freqtrade.optimize.hyperopt.get_timerange',
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
)
@ -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),
@ -427,7 +427,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
MagicMock(return_value=(MagicMock(), None)))
mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe',
'freqtrade.optimize.hyperopt.get_timerange',
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
)
@ -602,7 +602,7 @@ def test_generate_optimizer(mocker, default_conf) -> None:
MagicMock(return_value=backtest_result)
)
mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe',
'freqtrade.optimize.hyperopt.get_timerange',
MagicMock(return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13)))
)
patch_exchange(mocker)
@ -726,7 +726,7 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
MagicMock(return_value=(MagicMock(), None)))
mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe',
'freqtrade.optimize.hyperopt.get_timerange',
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
)
@ -769,7 +769,7 @@ def test_print_json_spaces_default(mocker, default_conf, caplog, capsys) -> None
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
MagicMock(return_value=(MagicMock(), None)))
mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe',
'freqtrade.optimize.hyperopt.get_timerange',
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
)
@ -811,7 +811,7 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) ->
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
MagicMock(return_value=(MagicMock(), None)))
mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe',
'freqtrade.optimize.hyperopt.get_timerange',
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
)
@ -851,7 +851,7 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
MagicMock(return_value=(MagicMock(), None)))
mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe',
'freqtrade.optimize.hyperopt.get_timerange',
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
)
@ -899,7 +899,7 @@ def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) -
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
MagicMock(return_value=(MagicMock(), None)))
mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe',
'freqtrade.optimize.hyperopt.get_timerange',
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
)
@ -930,7 +930,7 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None:
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
MagicMock(return_value=(MagicMock(), None)))
mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe',
'freqtrade.optimize.hyperopt.get_timerange',
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
)
@ -977,7 +977,7 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
MagicMock(return_value=(MagicMock(), None)))
mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe',
'freqtrade.optimize.hyperopt.get_timerange',
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
)
@ -1030,7 +1030,7 @@ def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, metho
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
MagicMock(return_value=(MagicMock(), None)))
mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe',
'freqtrade.optimize.hyperopt.get_timerange',
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
)

View File

@ -4,7 +4,7 @@ from unittest.mock import MagicMock, PropertyMock
import pytest
from freqtrade import OperationalException
from freqtrade.exceptions import OperationalException
from freqtrade.constants import AVAILABLE_PAIRLISTS
from freqtrade.resolvers import PairListResolver
from freqtrade.pairlist.pairlistmanager import PairListManager
@ -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):

View File

@ -7,13 +7,13 @@ from unittest.mock import ANY, MagicMock, PropertyMock
import pytest
from numpy import isnan
from freqtrade import DependencyException, TemporaryError
from freqtrade.edge import PairInfo
from freqtrade.exceptions import DependencyException, TemporaryError
from freqtrade.persistence import Trade
from freqtrade.rpc import RPC, RPCException
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.state import State
from tests.conftest import patch_get_signal, get_patched_freqtradebot
from tests.conftest import get_patched_freqtradebot, patch_get_signal
# Functions for recurrent object patching
@ -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,
)
@ -113,7 +113,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
rpc = RPC(freqtradebot)
freqtradebot.state = State.RUNNING
with pytest.raises(RPCException, match=r'.*no active order*'):
with pytest.raises(RPCException, match=r'.*no active trade*'):
rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
freqtradebot.create_trades()
@ -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)
@ -398,7 +398,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers):
get_valid_pair_combination=MagicMock(
side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}")
)
default_conf['dry_run'] = False
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False))
rpc = RPC(freqtradebot)
@ -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
)

View File

@ -230,6 +230,7 @@ def test_api_stopbuy(botclient):
def test_api_balance(botclient, mocker, rpc_balance):
ftbot, client = botclient
ftbot.config['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination',
side_effect=lambda a, b: f"{a}/{b}")
@ -255,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)
)
@ -291,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)
)
@ -307,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)
)
@ -322,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)
)
@ -380,7 +381,7 @@ def test_api_performance(botclient, mocker, ticker, fee):
close_rate=0.265441,
)
trade.close_profit = trade.calc_profit_percent()
trade.close_profit = trade.calc_profit_ratio()
Trade.session.add(trade)
trade = Trade(
@ -395,7 +396,7 @@ def test_api_performance(botclient, mocker, ticker, fee):
fee_open=fee.return_value,
close_rate=0.391
)
trade.close_profit = trade.calc_profit_percent()
trade.close_profit = trade.calc_profit_ratio()
Trade.session.add(trade)
Trade.session.flush()
@ -412,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)
)
@ -540,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)
)

View File

@ -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,
)
@ -275,13 +275,13 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
# Status table is also enabled when stopped
telegram._status_table(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'no active order' in msg_mock.call_args_list[0][0][0]
assert 'no active trade' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
freqtradebot.state = State.RUNNING
telegram._status_table(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert 'no active order' in msg_mock.call_args_list[0][0][0]
assert 'no active trade' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
# Create some test data
@ -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()
@ -462,7 +462,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None:
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination',
@ -494,6 +494,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick
def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={})
msg_mock = MagicMock()
@ -533,7 +534,8 @@ 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 "Running in Dry Run, balances are not available." in result
assert "*Warning:* Simulated balances in Dry Mode." in result
assert "Starting capital: `1000` BTC" in result
def test_balance_handle_too_large_response(default_conf, update, mocker) -> None:
@ -698,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,
)
@ -713,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()
@ -753,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,
)
@ -767,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()
@ -810,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
@ -961,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)
@ -996,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,
)

View File

@ -125,6 +125,7 @@ def test_min_roi_reached(default_conf, fee) -> None:
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=5,
open_date=arrow.utcnow().shift(hours=-1).datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
@ -162,6 +163,7 @@ def test_min_roi_reached2(default_conf, fee) -> None:
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=5,
open_date=arrow.utcnow().shift(hours=-1).datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
@ -195,6 +197,7 @@ def test_min_roi_reached3(default_conf, fee) -> None:
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=5,
open_date=arrow.utcnow().shift(hours=-1).datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
@ -299,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)

View File

@ -8,39 +8,42 @@ from pathlib import Path
import pytest
from pandas import DataFrame
from freqtrade import OperationalException
from freqtrade.exceptions import OperationalException
from freqtrade.resolvers import StrategyResolver
from freqtrade.strategy.interface import IStrategy
from tests.conftest import log_has, log_has_re
def test_search_strategy():
default_config = {}
default_location = Path(__file__).parent.parent.joinpath('strategy').resolve()
s, _ = StrategyResolver._search_object(
directory=default_location,
object_type=IStrategy,
kwargs={'config': default_config},
object_name='DefaultStrategy'
)
assert isinstance(s, IStrategy)
assert issubclass(s, IStrategy)
s, _ = StrategyResolver._search_object(
directory=default_location,
object_type=IStrategy,
kwargs={'config': default_config},
object_name='NotFoundStrategy'
)
assert s is None
def test_search_all_strategies():
directory = Path(__file__).parent
strategies = StrategyResolver.search_all_objects(directory)
assert isinstance(strategies, list)
assert len(strategies) == 3
assert isinstance(strategies[0], dict)
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 +51,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 +60,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 +74,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 +82,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 +117,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 +129,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 +141,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 +156,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 +175,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 +190,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 +210,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 +228,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 +243,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 +260,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 +268,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 +281,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 +293,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 +306,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 +318,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 +331,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 +340,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 +352,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

View File

@ -8,9 +8,8 @@ from pathlib import Path
from unittest.mock import MagicMock
import pytest
from jsonschema import Draft4Validator, ValidationError, validate
from jsonschema import ValidationError
from freqtrade import OperationalException, constants
from freqtrade.configuration import (Arguments, Configuration, check_exchange,
remove_credentials,
validate_config_consistency)
@ -20,6 +19,7 @@ from freqtrade.configuration.deprecated_settings import (
process_temporary_deprecated_settings)
from freqtrade.configuration.load_config import load_config_file
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
from freqtrade.exceptions import OperationalException
from freqtrade.loggers import _set_loggers, setup_logging
from freqtrade.state import RunMode
from tests.conftest import (log_has, log_has_re,
@ -718,7 +718,8 @@ def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None:
def test_validate_default_conf(default_conf) -> None:
validate(default_conf, constants.CONF_SCHEMA, Draft4Validator)
# Validate via our validator - we allow setting defaults!
validate_config_schema(default_conf)
def test_validate_tsl(default_conf):
@ -976,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", [

View File

@ -4,10 +4,10 @@ from unittest.mock import MagicMock
import pytest
from freqtrade import OperationalException
from freqtrade.configuration.directory_operations import (copy_sample_files,
create_datadir,
create_userdata_dir)
from freqtrade.exceptions import OperationalException
from tests.conftest import log_has, log_has_re

File diff suppressed because it is too large Load Diff

View File

@ -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,
@ -71,6 +71,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
)
mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock)
wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock())
mocker.patch("freqtrade.wallets.Wallets.get_free", MagicMock(return_value=1000))
freqtrade = get_patched_freqtradebot(mocker, default_conf)
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
@ -117,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,
@ -137,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)
@ -157,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

View File

@ -5,8 +5,8 @@ from unittest.mock import MagicMock, PropertyMock
import pytest
from freqtrade import OperationalException
from freqtrade.configuration import Arguments
from freqtrade.exceptions import OperationalException, FreqtradeException
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.main import main
from freqtrade.state import State
@ -79,6 +79,7 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=KeyboardInterrupt))
patched_configuration_load_config_file(mocker, default_conf)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.wallets.Wallets.update', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
args = ['trade', '-c', 'config.json.example']
@ -95,9 +96,10 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
mocker.patch(
'freqtrade.worker.Worker._worker',
MagicMock(side_effect=OperationalException('Oh snap!'))
MagicMock(side_effect=FreqtradeException('Oh snap!'))
)
patched_configuration_load_config_file(mocker, default_conf)
mocker.patch('freqtrade.wallets.Wallets.update', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
@ -120,6 +122,7 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None:
OperationalException("Oh snap!")])
mocker.patch('freqtrade.worker.Worker._worker', worker_mock)
patched_configuration_load_config_file(mocker, default_conf)
mocker.patch('freqtrade.wallets.Wallets.update', MagicMock())
reconfigure_mock = mocker.patch('freqtrade.worker.Worker._reconfigure', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -143,6 +146,7 @@ def test_reconfigure(mocker, default_conf) -> None:
'freqtrade.worker.Worker._worker',
MagicMock(side_effect=OperationalException('Oh snap!'))
)
mocker.patch('freqtrade.wallets.Wallets.update', MagicMock())
patched_configuration_load_config_file(mocker, default_conf)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())

View File

@ -6,7 +6,8 @@ import arrow
import pytest
from sqlalchemy import create_engine
from freqtrade import OperationalException, constants
from freqtrade import constants
from freqtrade.exceptions import OperationalException
from freqtrade.persistence import Trade, clean_dry_run_db, init
from tests.conftest import log_has
@ -100,7 +101,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")
@ -136,12 +137,13 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog):
id=2,
pair='ETH/BTC',
stake_amount=0.001,
open_rate=0.01,
amount=5,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
assert trade.open_order_id is None
assert trade.open_rate is None
assert trade.close_profit is None
assert trade.close_date is None
@ -173,6 +175,8 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog):
id=1,
pair='ETH/BTC',
stake_amount=0.001,
amount=5,
open_rate=0.01,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
@ -205,6 +209,8 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
open_rate=0.01,
amount=5,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
@ -212,7 +218,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee):
trade.open_order_id = 'something'
trade.update(limit_buy_order)
assert trade.calc_open_trade_price() == 0.0010024999999225068
assert trade._calc_open_trade_price() == 0.0010024999999225068
trade.update(limit_sell_order)
assert trade.calc_close_trade_price() == 0.0010646656050132426
@ -221,7 +227,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee):
assert trade.calc_profit() == 0.00006217
# Profit in percent
assert trade.calc_profit_percent() == 0.06201058
assert trade.calc_profit_ratio() == 0.06201058
@pytest.mark.usefixtures("init_persistence")
@ -229,6 +235,8 @@ def test_calc_close_trade_price_exception(limit_buy_order, fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
open_rate=0.1,
amount=5,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
@ -244,13 +252,14 @@ def test_update_open_order(limit_buy_order):
trade = Trade(
pair='ETH/BTC',
stake_amount=1.00,
open_rate=0.01,
amount=5,
fee_open=0.1,
fee_close=0.1,
exchange='bittrex',
)
assert trade.open_order_id is None
assert trade.open_rate is None
assert trade.close_profit is None
assert trade.close_date is None
@ -258,7 +267,6 @@ def test_update_open_order(limit_buy_order):
trade.update(limit_buy_order)
assert trade.open_order_id is None
assert trade.open_rate is None
assert trade.close_profit is None
assert trade.close_date is None
@ -268,6 +276,8 @@ def test_update_invalid_order(limit_buy_order):
trade = Trade(
pair='ETH/BTC',
stake_amount=1.00,
amount=5,
open_rate=0.001,
fee_open=0.1,
fee_close=0.1,
exchange='bittrex',
@ -282,6 +292,8 @@ def test_calc_open_trade_price(limit_buy_order, fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=5,
open_rate=0.00001099,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
@ -290,10 +302,10 @@ def test_calc_open_trade_price(limit_buy_order, fee):
trade.update(limit_buy_order) # Buy @ 0.00001099
# Get the open rate price with the standard fee rate
assert trade.calc_open_trade_price() == 0.0010024999999225068
assert trade._calc_open_trade_price() == 0.0010024999999225068
trade.fee_open = 0.003
# Get the open rate price with a custom fee rate
assert trade.calc_open_trade_price(fee=0.003) == 0.001002999999922468
assert trade._calc_open_trade_price() == 0.001002999999922468
@pytest.mark.usefixtures("init_persistence")
@ -301,6 +313,8 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=5,
open_rate=0.00001099,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
@ -324,6 +338,8 @@ def test_calc_profit(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=5,
open_rate=0.00001099,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
@ -352,10 +368,12 @@ def test_calc_profit(limit_buy_order, limit_sell_order, fee):
@pytest.mark.usefixtures("init_persistence")
def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee):
def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=5,
open_rate=0.00001099,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
@ -364,17 +382,17 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee):
trade.update(limit_buy_order) # Buy @ 0.00001099
# Get percent of profit with a custom rate (Higher than open rate)
assert trade.calc_profit_percent(rate=0.00001234) == 0.11723875
assert trade.calc_profit_ratio(rate=0.00001234) == 0.11723875
# Get percent of profit with a custom rate (Lower than open rate)
assert trade.calc_profit_percent(rate=0.00000123) == -0.88863828
assert trade.calc_profit_ratio(rate=0.00000123) == -0.88863828
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
trade.update(limit_sell_order)
assert trade.calc_profit_percent() == 0.06201058
assert trade.calc_profit_ratio() == 0.06201058
# Test with a custom fee rate on the close trade
assert trade.calc_profit_percent(fee=0.003) == 0.06147824
assert trade.calc_profit_ratio(fee=0.003) == 0.06147824
@pytest.mark.usefixtures("init_persistence")
@ -481,6 +499,7 @@ def test_migrate_old(mocker, default_conf, fee):
assert trade.max_rate == 0.0
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
assert trade.open_trade_price == trade._calc_open_trade_price()
def test_migrate_new(mocker, default_conf, fee, caplog):
@ -563,6 +582,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert log_has("trying trades_bak1", caplog)
assert log_has("trying trades_bak2", caplog)
assert log_has("Running database migration - backup available as trades_bak2", caplog)
assert trade.open_trade_price == trade._calc_open_trade_price()
def test_migrate_mid_state(mocker, default_conf, fee, caplog):
@ -622,6 +642,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
assert trade.max_rate == 0.0
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
assert trade.open_trade_price == trade._calc_open_trade_price()
assert log_has("trying trades_bak0", caplog)
assert log_has("Running database migration - backup available as trades_bak0", caplog)
@ -630,6 +651,7 @@ def test_adjust_stop_loss(fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=5,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
@ -681,6 +703,7 @@ def test_adjust_min_max_rates(fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=5,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',

View File

@ -7,17 +7,17 @@ import plotly.graph_objects as go
import pytest
from plotly.subplots import make_subplots
from freqtrade import OperationalException
from freqtrade.configuration import TimeRange
from freqtrade.data import history
from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data
from freqtrade.exceptions import OperationalException
from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit
from freqtrade.plot.plotting import (add_indicators, add_profit,
load_and_plot_trades,
generate_candlestick_graph,
generate_plot_filename,
generate_profit_graph, init_plotscript,
plot_profit, plot_trades, store_plot_file)
load_and_plot_trades, plot_profit,
plot_trades, store_plot_file)
from freqtrade.strategy.default_strategy import DefaultStrategy
from tests.conftest import get_args, log_has, log_has_re

View File

@ -4,14 +4,15 @@ from unittest.mock import MagicMock, PropertyMock
import pytest
from freqtrade import OperationalException
from freqtrade.exceptions import OperationalException
from freqtrade.state import RunMode
from freqtrade.utils import (setup_utils_configuration, start_create_userdir,
start_download_data, start_list_exchanges,
start_list_markets, start_list_timeframes,
start_new_hyperopt, start_new_strategy,
start_test_pairlist, start_trading,
start_hyperopt_list, start_hyperopt_show)
start_download_data, start_hyperopt_list,
start_hyperopt_show, start_list_exchanges,
start_list_markets, start_list_strategies,
start_list_timeframes, start_new_hyperopt,
start_new_strategy, start_test_pairlist,
start_trading)
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
patched_configuration_load_config_file)
@ -444,6 +445,12 @@ def test_create_datadir_failed(caplog):
def test_create_datadir(caplog, mocker):
# Ensure that caplog is empty before starting ...
# Should prevent random failures.
caplog.clear()
# Added assert here to analyze random test-failures ...
assert len(caplog.record_tuples) == 0
cud = mocker.patch("freqtrade.utils.create_userdata_dir", MagicMock())
csf = mocker.patch("freqtrade.utils.copy_sample_files", MagicMock())
args = [
@ -627,6 +634,37 @@ def test_download_data_trades(mocker, caplog):
assert convert_mock.call_count == 1
def test_start_list_strategies(mocker, caplog, capsys):
args = [
"list-strategies",
"--strategy-path",
str(Path(__file__).parent / "strategy"),
"-1"
]
pargs = get_args(args)
# pargs['config'] = None
start_list_strategies(pargs)
captured = capsys.readouterr()
assert "TestStrategyLegacy" in captured.out
assert "legacy_strategy.py" not in captured.out
assert "DefaultStrategy" in captured.out
# Test regular output
args = [
"list-strategies",
"--strategy-path",
str(Path(__file__).parent / "strategy"),
]
pargs = get_args(args)
# pargs['config'] = None
start_list_strategies(pargs)
captured = capsys.readouterr()
assert "TestStrategyLegacy" in captured.out
assert "legacy_strategy.py" in captured.out
assert "DefaultStrategy" in captured.out
def test_start_test_pairlist(mocker, caplog, markets, tickers, default_conf, capsys):
mocker.patch.multiple('freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),

View File

@ -1,7 +1,8 @@
# pragma pylint: disable=missing-docstring
from tests.conftest import get_patched_freqtradebot
from unittest.mock import MagicMock
from tests.conftest import get_patched_freqtradebot
def test_sync_wallet_at_boot(mocker, default_conf):
default_conf['dry_run'] = False