Merge branch 'feat/short' into lev-exchange

This commit is contained in:
Sam Germain 2021-09-04 20:15:36 -06:00
commit 7eab855476
95 changed files with 1757 additions and 1148 deletions

View File

@ -1,4 +1,4 @@
FROM python:3.9.6-slim-buster as base FROM python:3.9.7-slim-buster as base
# Setup env # Setup env
ENV LANG C.UTF-8 ENV LANG C.UTF-8

View File

@ -37,7 +37,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
Exchanges confirmed working by the community: Exchanges confirmed working by the community:
- [X] [Bitvavo](https://bitvavo.com/) - [X] [Bitvavo](https://bitvavo.com/)
- [X] [Kukoin](https://www.kucoin.com/) - [X] [Kucoin](https://www.kucoin.com/)
## Documentation ## Documentation

View File

@ -42,7 +42,7 @@ docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_I
docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM
# Run backtest # Run backtest
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy DefaultStrategy docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV2
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "failed running backtest" echo "failed running backtest"
@ -74,7 +74,5 @@ fi
docker images docker images
if [ $? -ne 0 ]; then # Cleanup old images from arm64 node.
echo "failed building image" docker image prune -a --force --filter "until=24h"
return 1
fi

View File

@ -53,7 +53,7 @@ docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE
docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT
# Run backtest # Run backtest
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy DefaultStrategy docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV2
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "failed running backtest" echo "failed running backtest"

View File

@ -174,7 +174,7 @@
"heartbeat_interval": 60 "heartbeat_interval": 60
}, },
"disable_dataframe_checks": false, "disable_dataframe_checks": false,
"strategy": "DefaultStrategy", "strategy": "SampleStrategy",
"strategy_path": "user_data/strategies/", "strategy_path": "user_data/strategies/",
"dataformat_ohlcv": "json", "dataformat_ohlcv": "json",
"dataformat_trades": "jsongz" "dataformat_trades": "jsongz"

View File

@ -335,7 +335,7 @@ Once the optimized parameters and conditions have been implemented into your str
To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
Should results don't match, please double-check to make sure you transferred all conditions correctly. Should results not match, please double-check to make sure you transferred all conditions correctly.
Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy. Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy.
You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`). You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`).

View File

@ -18,6 +18,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-p PAIRS [PAIRS ...]] [--eps] [--dmmp] [-p PAIRS [PAIRS ...]] [--eps] [--dmmp]
[--enable-protections] [--enable-protections]
[--dry-run-wallet DRY_RUN_WALLET] [--dry-run-wallet DRY_RUN_WALLET]
[--timeframe-detail TIMEFRAME_DETAIL]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
[--export {none,trades}] [--export-filename PATH] [--export {none,trades}] [--export-filename PATH]
@ -55,6 +56,9 @@ optional arguments:
--dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET
Starting balance, used for backtesting / hyperopt and Starting balance, used for backtesting / hyperopt and
dry-runs. dry-runs.
--timeframe-detail TIMEFRAME_DETAIL
Specify detail timeframe for backtesting (`1m`, `5m`,
`30m`, `1h`, `1d`).
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...] --strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
Provide a space-separated list of strategies to Provide a space-separated list of strategies to
backtest. Please note that ticker-interval needs to be backtest. Please note that ticker-interval needs to be
@ -62,7 +66,7 @@ optional arguments:
this together with `--export trades`, the strategy- this together with `--export trades`, the strategy-
name is injected into the filename (so `backtest- name is injected into the filename (so `backtest-
data.json` becomes `backtest-data- data.json` becomes `backtest-data-
DefaultStrategy.json` SampleStrategy.json`
--export {none,trades} --export {none,trades}
Export backtest results (default: trades). Export backtest results (default: trades).
--export-filename PATH --export-filename PATH
@ -425,7 +429,12 @@ It contains some useful key metrics about performance of your strategy on backte
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). - `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column. - `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
### Assumptions made by backtesting ### Further backtest-result analysis
To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file).
You can then load the trades to perform further analysis as shown in our [data analysis](data-analysis.md#backtesting) backtesting section.
## Assumptions made by backtesting
Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions: Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions:
@ -456,10 +465,30 @@ Also, keep in mind that past results don't guarantee future success.
In addition to the above assumptions, strategy authors should carefully read the [Common Mistakes](strategy-customization.md#common-mistakes-when-developing-strategies) section, to avoid using data in backtesting which is not available in real market conditions. In addition to the above assumptions, strategy authors should carefully read the [Common Mistakes](strategy-customization.md#common-mistakes-when-developing-strategies) section, to avoid using data in backtesting which is not available in real market conditions.
### Further backtest-result analysis ### Improved backtest accuracy
To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file). One big limitation of backtesting is it's inability to know how prices moved intra-candle (was high before close, or viceversa?).
You can then load the trades to perform further analysis as shown in our [data analysis](data-analysis.md#backtesting) backtesting section. So assuming you run backtesting with a 1h timeframe, there will be 4 prices for that candle (Open, High, Low, Close).
While backtesting does take some assumptions (read above) about this - this can never be perfect, and will always be biased in one way or the other.
To mitigate this, freqtrade can use a lower (faster) timeframe to simulate intra-candle movements.
To utilize this, you can append `--timeframe-detail 5m` to your regular backtesting command.
``` bash
freqtrade backtesting --strategy AwesomeStrategy --timeframe 1h --timeframe-detail 5m
```
This will load 1h data as well as 5m data for the timeframe. The strategy will be analyzed with the 1h timeframe - and for every "open trade candle" (candles where a trade is open) the 5m data will be used to simulate intra-candle movements.
All callback functions (`custom_sell()`, `custom_stoploss()`, ... ) will be running for each 5m candle once the trade is opened (so 12 times in the above example of 1h timeframe, and 5m detailed timeframe).
`--timeframe-detail` must be smaller than the original timeframe, otherwise backtesting will fail to start.
Obviously this will require more memory (5m data is bigger than 1h data), and will also impact runtime (depending on the amount of trades and trade durations).
Also, data must be available / downloaded already.
!!! Tip
You can use this function as the last part of strategy development, to ensure your strategy is not exploiting one of the [backtesting assumptions](#assumptions-made-by-backtesting). Strategies that perform similarly well with this mode have a good chance to perform well in dry/live modes too (although only forward-testing (dry-mode) can really confirm a strategy).
## Backtesting multiple strategies ## Backtesting multiple strategies

View File

@ -7,7 +7,7 @@ This page provides you some basic concepts on how Freqtrade works and operates.
* **Strategy**: Your trading strategy, telling the bot what to do. * **Strategy**: Your trading strategy, telling the bot what to do.
* **Trade**: Open position. * **Trade**: Open position.
* **Open Order**: Order which is currently placed on the exchange, and is not yet complete. * **Open Order**: Order which is currently placed on the exchange, and is not yet complete.
* **Pair**: Tradable pair, usually in the format of Quote/Base (e.g. XRP/USDT). * **Pair**: Tradable pair, usually in the format of Base/Quote (e.g. XRP/USDT).
* **Timeframe**: Candle length to use (e.g. `"5m"`, `"1h"`, ...). * **Timeframe**: Candle length to use (e.g. `"5m"`, `"1h"`, ...).
* **Indicators**: Technical indicators (SMA, EMA, RSI, ...). * **Indicators**: Technical indicators (SMA, EMA, RSI, ...).
* **Limit order**: Limit orders which execute at the defined limit price or better. * **Limit order**: Limit orders which execute at the defined limit price or better.
@ -35,12 +35,13 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and
* Calls `check_buy_timeout()` strategy callback for open buy orders. * Calls `check_buy_timeout()` strategy callback for open buy orders.
* Calls `check_sell_timeout()` strategy callback for open sell orders. * Calls `check_sell_timeout()` strategy callback for open sell orders.
* Verifies existing positions and eventually places sell orders. * Verifies existing positions and eventually places sell orders.
* Considers stoploss, ROI and sell-signal. * Considers stoploss, ROI and sell-signal, `custom_sell()` and `custom_stoploss()`.
* Determine sell-price based on `ask_strategy` configuration setting. * Determine sell-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback.
* Before a sell order is placed, `confirm_trade_exit()` strategy callback is called. * Before a sell order is placed, `confirm_trade_exit()` strategy callback is called.
* Check if trade-slots are still available (if `max_open_trades` is reached). * Check if trade-slots are still available (if `max_open_trades` is reached).
* Verifies buy signal trying to enter new positions. * Verifies buy signal trying to enter new positions.
* Determine buy-price based on `bid_strategy` configuration setting. * Determine buy-price based on `bid_strategy` configuration setting, or by using the `custom_entry_price()` callback.
* Determine stake size by calling the `custom_stake_amount()` callback.
* Before a buy order is placed, `confirm_trade_entry()` strategy callback is called. * Before a buy order is placed, `confirm_trade_entry()` strategy callback is called.
This loop will be repeated again and again until the bot is stopped. This loop will be repeated again and again until the bot is stopped.
@ -52,9 +53,10 @@ This loop will be repeated again and again until the bot is stopped.
* Load historic data for configured pairlist. * Load historic data for configured pairlist.
* Calls `bot_loop_start()` once. * Calls `bot_loop_start()` once.
* Calculate indicators (calls `populate_indicators()` once per pair). * Calculate indicators (calls `populate_indicators()` once per pair).
* Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair) * Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair).
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy)
* Loops per candle simulating entry and exit points. * Loops per candle simulating entry and exit points.
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
* Call `custom_stoploss()` and `custom_sell()` to find custom exit points.
* Generate backtest report output * Generate backtest report output
!!! Note !!! Note

View File

@ -105,11 +105,12 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `ask_strategy.order_book_top` | Bot will use the top N rate in Order Book "price_side" to sell. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Asks](#sell-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer | `ask_strategy.order_book_top` | Bot will use the top N rate in Order Book "price_side" to sell. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Asks](#sell-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `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 | `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
| `sell_profit_only` | Wait until the bot reaches `sell_profit_offset` before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `sell_profit_only` | Wait until the bot reaches `sell_profit_offset` before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `sell_profit_offset` | Sell-signal is only active above this value. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0`.* <br> **Datatype:** Float (as ratio) | `sell_profit_offset` | Sell-signal is only active above this value. Only active in combination with `sell_profit_only=True`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0`.* <br> **Datatype:** Float (as ratio)
| `ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used. <br> **Datatype:** Integer | `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used. <br> **Datatype:** Integer
| `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict | `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict
| `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 | `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
| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price. <br>*Defaults to `0.02` 2%).*<br> **Datatype:** Positive float
| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String | `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.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.<br>**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

View File

@ -240,11 +240,18 @@ The `IProtection` parent class provides a helper method for this in `calculate_l
!!! Note !!! Note
This section is a Work in Progress and is not a complete guide on how to test a new exchange with Freqtrade. This section is a Work in Progress and is not a complete guide on how to test a new exchange with Freqtrade.
!!! Note
Make sure to use an up-to-date version of CCXT before running any of the below tests.
You can get the latest version of ccxt by running `pip install -U ccxt` with activated virtual environment.
Native docker is not supported for these tests, however the available dev-container will support all required actions and eventually necessary changes.
Most exchanges supported by CCXT should work out of the box. Most exchanges supported by CCXT should work out of the box.
To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun tests/exchange/test_ccxt_compat.py`. To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun tests/exchange/test_ccxt_compat.py`.
Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar). Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar).
Also try to use `freqtrade download-data` for an extended timerange and verify that the data downloaded correctly (no holes, the specified timerange was actually downloaded).
### Stoploss On Exchange ### Stoploss On Exchange
Check if the new exchange supports Stoploss on Exchange orders through their API. Check if the new exchange supports Stoploss on Exchange orders through their API.

View File

@ -105,7 +105,7 @@ To use subaccounts with FTX, you need to edit the configuration and add the foll
## Kucoin ## Kucoin
Kucoin requries a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows: Kucoin requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
```json ```json
"exchange": { "exchange": {

View File

@ -48,7 +48,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--hyperopt-path PATH] [--eps] [--dmmp] [--hyperopt-path PATH] [--eps] [--dmmp]
[--enable-protections] [--enable-protections]
[--dry-run-wallet DRY_RUN_WALLET] [-e INT] [--dry-run-wallet DRY_RUN_WALLET] [-e INT]
[--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] [--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]]
[--print-all] [--no-color] [--print-json] [-j JOBS] [--print-all] [--no-color] [--print-json] [-j JOBS]
[--random-state INT] [--min-trades INT] [--random-state INT] [--min-trades INT]
[--hyperopt-loss NAME] [--disable-param-export] [--hyperopt-loss NAME] [--disable-param-export]
@ -92,7 +92,7 @@ optional arguments:
Starting balance, used for backtesting / hyperopt and Starting balance, used for backtesting / hyperopt and
dry-runs. dry-runs.
-e INT, --epochs INT Specify number of epochs (default: 100). -e INT, --epochs INT Specify number of epochs (default: 100).
--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...] --spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]
Specify which parameters to hyperopt. Space-separated Specify which parameters to hyperopt. Space-separated
list. list.
--print-all Print all results, not only the best ones. --print-all Print all results, not only the best ones.
@ -456,7 +456,7 @@ class MyAwesomeStrategy(IStrategy):
"only_per_pair": False "only_per_pair": False
}) })
return protection return prot
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# ... # ...
@ -576,7 +576,8 @@ Legal values are:
* `roi`: just optimize the minimal profit table for your strategy * `roi`: just optimize the minimal profit table for your strategy
* `stoploss`: search for the best stoploss value * `stoploss`: search for the best stoploss value
* `trailing`: search for the best trailing stop values * `trailing`: search for the best trailing stop values
* `default`: `all` except `trailing` * `protection`: search for the best protection parameters (read the [protections section](#optimizing-protections) on how to properly define these)
* `default`: `all` except `trailing` and `protection`
* space-separated list of any of the above values for example `--spaces roi stoploss` * space-separated list of any of the above values for example `--spaces roi stoploss`
The default Hyperopt Search Space, used when no `--space` command line option is specified, does not include the `trailing` hyperspace. We recommend you to run optimization for the `trailing` hyperspace separately, when the best parameters for other hyperspaces were found, validated and pasted into your custom strategy. The default Hyperopt Search Space, used when no `--space` command line option is specified, does not include the `trailing` hyperspace. We recommend you to run optimization for the `trailing` hyperspace separately, when the best parameters for other hyperspaces were found, validated and pasted into your custom strategy.
@ -826,8 +827,8 @@ After you run Hyperopt for the desired amount of epochs, you can later list all
Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected.
To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
Should results don't match, please double-check to make sure you transferred all conditions correctly. Should results not match, please double-check to make sure you transferred all conditions correctly.
Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy. Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy.
You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`). You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`).

View File

@ -58,7 +58,7 @@ This option must be configured along with `exchange.skip_pair_validation` in the
When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume. When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume.
When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange. When used in the leading position of the chain of Pairlist Handlers, the `pair_whitelist` configuration setting is ignored. Instead, `VolumePairList` selects the top assets from all available markets with matching stake-currency on the exchange.
The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes).
The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists. The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists.
@ -74,11 +74,14 @@ Filtering instances (not the first position in the list) will not apply any cach
"method": "VolumePairList", "method": "VolumePairList",
"number_assets": 20, "number_assets": 20,
"sort_key": "quoteVolume", "sort_key": "quoteVolume",
"min_value": 0,
"refresh_period": 1800 "refresh_period": 1800
} }
], ],
``` ```
You can define a minimum volume with `min_value` - which will filter out pairs with a volume lower than the specified value in the specified timerange.
`VolumePairList` can also operate in an advanced mode to build volume over a given timerange of specified candle size. It utilizes exchange historical candle data, builds a typical price (calculated by (open+high+low)/3) and multiplies the typical price with every candle's volume. The sum is the `quoteVolume` over the given range. This allows different scenarios, for a more smoothened volume, when using longer ranges with larger candle sizes, or the opposite when using a short range with small candles. `VolumePairList` can also operate in an advanced mode to build volume over a given timerange of specified candle size. It utilizes exchange historical candle data, builds a typical price (calculated by (open+high+low)/3) and multiplies the typical price with every candle's volume. The sum is the `quoteVolume` over the given range. This allows different scenarios, for a more smoothened volume, when using longer ranges with larger candle sizes, or the opposite when using a short range with small candles.
For convenience `lookback_days` can be specified, which will imply that 1d candles will be used for the lookback. In the example below the pairlist would be created based on the last 7 days: For convenience `lookback_days` can be specified, which will imply that 1d candles will be used for the lookback. In the example below the pairlist would be created based on the last 7 days:
@ -89,6 +92,7 @@ For convenience `lookback_days` can be specified, which will imply that 1d candl
"method": "VolumePairList", "method": "VolumePairList",
"number_assets": 20, "number_assets": 20,
"sort_key": "quoteVolume", "sort_key": "quoteVolume",
"min_value": 0,
"refresh_period": 86400, "refresh_period": 86400,
"lookback_days": 7 "lookback_days": 7
} }
@ -109,6 +113,7 @@ More sophisticated approach can be used, by using `lookback_timeframe` for candl
"method": "VolumePairList", "method": "VolumePairList",
"number_assets": 20, "number_assets": 20,
"sort_key": "quoteVolume", "sort_key": "quoteVolume",
"min_value": 0,
"refresh_period": 3600, "refresh_period": 3600,
"lookback_timeframe": "1h", "lookback_timeframe": "1h",
"lookback_period": 72 "lookback_period": 72

View File

@ -47,7 +47,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
Exchanges confirmed working by the community: Exchanges confirmed working by the community:
- [X] [Bitvavo](https://bitvavo.com/) - [X] [Bitvavo](https://bitvavo.com/)
- [X] [Kukoin](https://www.kucoin.com/) - [X] [Kucoin](https://www.kucoin.com/)
## Requirements ## Requirements

View File

@ -1,4 +1,4 @@
mkdocs==1.2.2 mkdocs==1.2.2
mkdocs-material==7.2.2 mkdocs-material==7.2.5
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2
pymdown-extensions==8.2 pymdown-extensions==8.2

View File

@ -357,6 +357,55 @@ See [Dataframe access](#dataframe-access) for more information about dataframe u
--- ---
## Custom order price rules
By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits.
!!! Note
If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration.
### Custom order entry and exit price example
``` python
from datetime import datetime, timedelta, timezone
from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy):
# ... populate_* methods
def custom_entry_price(self, pair: str, current_time: datetime,
proposed_rate, **kwargs) -> float:
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
timeframe=self.timeframe)
new_entryprice = dataframe['bollinger_10_lowerband'].iat[-1]
return new_entryprice
def custom_exit_price(self, pair: str, trade: Trade,
current_time: datetime, proposed_rate: float,
current_profit: float, **kwargs) -> float:
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
timeframe=self.timeframe)
new_exitprice = dataframe['bollinger_10_upperband'].iat[-1]
return new_exitprice
```
!!! Warning
Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter.
!!! Example
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98.
!!! Warning "No backtesting support"
Custom entry-prices are currently not supported during backtesting.
## Custom order timeout rules ## Custom order timeout rules
Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.

View File

@ -228,7 +228,7 @@ graph = generate_candlestick_graph(pair=pair,
# Show graph inline # Show graph inline
# graph.show() # graph.show()
# Render graph in a seperate window # Render graph in a separate window
graph.show(renderer="browser") graph.show(renderer="browser")
``` ```

View File

@ -22,7 +22,7 @@ ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv",
"max_open_trades", "stake_amount", "fee", "pairs"] "max_open_trades", "stake_amount", "fee", "pairs"]
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
"enable_protections", "dry_run_wallet", "enable_protections", "dry_run_wallet", "timeframe_detail",
"strategy_list", "export", "exportfilename"] "strategy_list", "export", "exportfilename"]
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",

View File

@ -66,16 +66,22 @@ def ask_user_config() -> Dict[str, Any]:
{ {
"type": "text", "type": "text",
"name": "stake_amount", "name": "stake_amount",
"message": "Please insert your stake amount:", "message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):",
"default": "0.01", "default": "0.01",
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val), "validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val),
"filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"'
if val == UNLIMITED_STAKE_AMOUNT
else val
}, },
{ {
"type": "text", "type": "text",
"name": "max_open_trades", "name": "max_open_trades",
"message": f"Please insert max_open_trades (Integer or '{UNLIMITED_STAKE_AMOUNT}'):", "message": f"Please insert max_open_trades (Integer or '{UNLIMITED_STAKE_AMOUNT}'):",
"default": "3", "default": "3",
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_int(val) "validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_int(val),
"filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"'
if val == UNLIMITED_STAKE_AMOUNT
else val
}, },
{ {
"type": "text", "type": "text",

View File

@ -135,6 +135,10 @@ AVAILABLE_CLI_OPTIONS = {
help='Override the value of the `stake_amount` configuration setting.', help='Override the value of the `stake_amount` configuration setting.',
), ),
# Backtesting # Backtesting
"timeframe_detail": Arg(
'--timeframe-detail',
help='Specify detail timeframe for backtesting (`1m`, `5m`, `30m`, `1h`, `1d`).',
),
"position_stacking": Arg( "position_stacking": Arg(
'--eps', '--enable-position-stacking', '--eps', '--enable-position-stacking',
help='Allow buying the same pair multiple times (position stacking).', help='Allow buying the same pair multiple times (position stacking).',
@ -162,7 +166,7 @@ AVAILABLE_CLI_OPTIONS = {
'Please note that ticker-interval needs to be set either in config ' 'Please note that ticker-interval needs to be set either in config '
'or via command line. When using this together with `--export trades`, ' 'or via command line. When using this together with `--export trades`, '
'the strategy-name is injected into the filename ' 'the strategy-name is injected into the filename '
'(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`', '(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`',
nargs='+', nargs='+',
), ),
"export": Arg( "export": Arg(

View File

@ -74,8 +74,6 @@ def start_new_strategy(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
if "strategy" in args and args["strategy"]: if "strategy" in args and args["strategy"]:
if args["strategy"] == "DefaultStrategy":
raise OperationalException("DefaultStrategy is not allowed as name.")
new_path = config['user_data_dir'] / USERPATH_STRATEGIES / (args['strategy'] + '.py') new_path = config['user_data_dir'] / USERPATH_STRATEGIES / (args['strategy'] + '.py')
@ -128,8 +126,6 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
if 'hyperopt' in args and args['hyperopt']: if 'hyperopt' in args and args['hyperopt']:
if args['hyperopt'] == 'DefaultHyperopt':
raise OperationalException("DefaultHyperopt is not allowed as name.")
new_path = config['user_data_dir'] / USERPATH_HYPEROPTS / (args['hyperopt'] + '.py') new_path = config['user_data_dir'] / USERPATH_HYPEROPTS / (args['hyperopt'] + '.py')

View File

@ -1,6 +1,6 @@
import logging import logging
from operator import itemgetter from operator import itemgetter
from typing import Any, Dict, List from typing import Any, Dict
from colorama import init as colorama_init from colorama import init as colorama_init
@ -28,30 +28,12 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
no_details = config.get('hyperopt_list_no_details', False) no_details = config.get('hyperopt_list_no_details', False)
no_header = False no_header = False
filteroptions = {
'only_best': config.get('hyperopt_list_best', False),
'only_profitable': config.get('hyperopt_list_profitable', False),
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
'filter_max_objective': config.get('hyperopt_list_max_objective', None),
}
results_file = get_latest_hyperopt_file( results_file = get_latest_hyperopt_file(
config['user_data_dir'] / 'hyperopt_results', config['user_data_dir'] / 'hyperopt_results',
config.get('hyperoptexportfilename')) config.get('hyperoptexportfilename'))
# Previous evaluations # Previous evaluations
epochs = HyperoptTools.load_previous_results(results_file) epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
total_epochs = len(epochs)
epochs = hyperopt_filter_epochs(epochs, filteroptions)
if print_colorized: if print_colorized:
colorama_init(autoreset=True) colorama_init(autoreset=True)
@ -59,7 +41,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
if not export_csv: if not export_csv:
try: try:
print(HyperoptTools.get_result_table(config, epochs, total_epochs, print(HyperoptTools.get_result_table(config, epochs, total_epochs,
not filteroptions['only_best'], not config.get('hyperopt_list_best', False),
print_colorized, 0)) print_colorized, 0))
except KeyboardInterrupt: except KeyboardInterrupt:
print('User interrupted..') print('User interrupted..')
@ -71,7 +53,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
if epochs and export_csv: if epochs and export_csv:
HyperoptTools.export_csv_file( HyperoptTools.export_csv_file(
config, epochs, total_epochs, not filteroptions['only_best'], export_csv config, epochs, total_epochs, not config.get('hyperopt_list_best', False), export_csv
) )
@ -91,26 +73,9 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
n = config.get('hyperopt_show_index', -1) n = config.get('hyperopt_show_index', -1)
filteroptions = {
'only_best': config.get('hyperopt_list_best', False),
'only_profitable': config.get('hyperopt_list_profitable', False),
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
'filter_max_objective': config.get('hyperopt_list_max_objective', None)
}
# Previous evaluations # Previous evaluations
epochs = HyperoptTools.load_previous_results(results_file) epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
total_epochs = len(epochs)
epochs = hyperopt_filter_epochs(epochs, filteroptions)
filtered_epochs = len(epochs) filtered_epochs = len(epochs)
if n > filtered_epochs: if n > filtered_epochs:
@ -137,138 +102,3 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header, HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header,
header_str="Epoch details") header_str="Epoch details")
def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
"""
Filter our items from the list of hyperopt results
TODO: after 2021.5 remove all "legacy" mode queries.
"""
if filteroptions['only_best']:
epochs = [x for x in epochs if x['is_best']]
if filteroptions['only_profitable']:
epochs = [x for x in epochs if x['results_metrics'].get(
'profit', x['results_metrics'].get('profit_total', 0)) > 0]
epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions)
epochs = _hyperopt_filter_epochs_duration(epochs, filteroptions)
epochs = _hyperopt_filter_epochs_profit(epochs, filteroptions)
epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions)
logger.info(f"{len(epochs)} " +
("best " if filteroptions['only_best'] else "") +
("profitable " if filteroptions['only_profitable'] else "") +
"epochs found.")
return epochs
def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int):
"""
Filter epochs with trade-counts > trades
"""
return [
x for x in epochs
if x['results_metrics'].get(
'trade_count', x['results_metrics'].get('total_trades', 0)
) > trade_count
]
def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List:
if filteroptions['filter_min_trades'] > 0:
epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades'])
if filteroptions['filter_max_trades'] > 0:
epochs = [
x for x in epochs
if x['results_metrics'].get(
'trade_count', x['results_metrics'].get('total_trades')
) < filteroptions['filter_max_trades']
]
return epochs
def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List:
def get_duration_value(x):
# Duration in minutes ...
if 'duration' in x['results_metrics']:
return x['results_metrics']['duration']
else:
# New mode
if 'holding_avg_s' in x['results_metrics']:
avg = x['results_metrics']['holding_avg_s']
return avg // 60
raise OperationalException(
"Holding-average not available. Please omit the filter on average time, "
"or rerun hyperopt with this version")
if filteroptions['filter_min_avg_time'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if get_duration_value(x) > filteroptions['filter_min_avg_time']
]
if filteroptions['filter_max_avg_time'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if get_duration_value(x) < filteroptions['filter_max_avg_time']
]
return epochs
def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
if filteroptions['filter_min_avg_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics'].get(
'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100
) > filteroptions['filter_min_avg_profit']
]
if filteroptions['filter_max_avg_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics'].get(
'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100
) < filteroptions['filter_max_avg_profit']
]
if filteroptions['filter_min_total_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics'].get(
'profit', x['results_metrics'].get('profit_total_abs', 0)
) > filteroptions['filter_min_total_profit']
]
if filteroptions['filter_max_total_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics'].get(
'profit', x['results_metrics'].get('profit_total_abs', 0)
) < filteroptions['filter_max_total_profit']
]
return epochs
def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List:
if filteroptions['filter_min_objective'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']]
if filteroptions['filter_max_objective'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']]
return epochs

View File

@ -242,6 +242,9 @@ class Configuration:
except ValueError: except ValueError:
pass pass
self._args_to_config(config, argname='timeframe_detail',
logstring='Parameter --timeframe-detail detected, '
'using {} for intra-candle backtesting ...')
self._args_to_config(config, argname='stake_amount', self._args_to_config(config, argname='stake_amount',
logstring='Parameter --stake-amount detected, ' logstring='Parameter --stake-amount detected, '
'overriding stake_amount to: {} ...') 'overriding stake_amount to: {} ...')

View File

@ -49,6 +49,8 @@ USERPATH_NOTEBOOKS = 'notebooks'
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
ENV_VAR_PREFIX = 'FREQTRADE__' ENV_VAR_PREFIX = 'FREQTRADE__'
NON_OPEN_EXCHANGE_STATES = ('cancelled', 'canceled', 'closed', 'expired')
# Define decimals per coin for outputs # Define decimals per coin for outputs
# Only used for outputs. # Only used for outputs.
@ -191,6 +193,9 @@ CONF_SCHEMA = {
}, },
'required': ['price_side'] 'required': ['price_side']
}, },
'custom_price_max_distance_ratio': {
'type': 'number', 'minimum': 0.0
},
'order_types': { 'order_types': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {

View File

@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
BT_DATA_COLUMNS_OLD = ["pair", "profit_percent", "open_date", "close_date", "index", BT_DATA_COLUMNS_OLD = ["pair", "profit_percent", "open_date", "close_date", "index",
"trade_duration", "open_rate", "close_rate", "open_at_end", "sell_reason"] "trade_duration", "open_rate", "close_rate", "open_at_end", "sell_reason"]
# Mid-term format, crated by BacktestResult Named Tuple # Mid-term format, created by BacktestResult Named Tuple
BT_DATA_COLUMNS_MID = ['pair', 'profit_percent', 'open_date', 'close_date', 'trade_duration', BT_DATA_COLUMNS_MID = ['pair', 'profit_percent', 'open_date', 'close_date', 'trade_duration',
'open_rate', 'close_rate', 'open_at_end', 'sell_reason', 'fee_open', 'open_rate', 'close_rate', 'open_at_end', 'sell_reason', 'fee_open',
'fee_close', 'amount', 'profit_abs', 'profit_ratio'] 'fee_close', 'amount', 'profit_abs', 'profit_ratio']

View File

@ -242,7 +242,7 @@ def convert_trades_format(config: Dict[str, Any], convert_from: str, convert_to:
:param config: Config dictionary :param config: Config dictionary
:param convert_from: Source format :param convert_from: Source format
:param convert_to: Target format :param convert_to: Target format
:param erase: Erase souce data (does not apply if source and target format are identical) :param erase: Erase source data (does not apply if source and target format are identical)
""" """
from freqtrade.data.history.idatahandler import get_datahandler from freqtrade.data.history.idatahandler import get_datahandler
src = get_datahandler(config['datadir'], convert_from) src = get_datahandler(config['datadir'], convert_from)
@ -267,7 +267,7 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to:
:param config: Config dictionary :param config: Config dictionary
:param convert_from: Source format :param convert_from: Source format
:param convert_to: Target format :param convert_to: Target format
:param erase: Erase souce data (does not apply if source and target format are identical) :param erase: Erase source data (does not apply if source and target format are identical)
""" """
from freqtrade.data.history.idatahandler import get_datahandler from freqtrade.data.history.idatahandler import get_datahandler
src = get_datahandler(config['datadir'], convert_from) src = get_datahandler(config['datadir'], convert_from)

View File

@ -117,10 +117,11 @@ def refresh_data(datadir: Path,
:param timerange: Limit data to be loaded to this timerange :param timerange: Limit data to be loaded to this timerange
""" """
data_handler = get_datahandler(datadir, data_format) data_handler = get_datahandler(datadir, data_format)
for pair in pairs: for idx, pair in enumerate(pairs):
_download_pair_history(pair=pair, timeframe=timeframe, process = f'{idx}/{len(pairs)}'
datadir=datadir, timerange=timerange, _download_pair_history(pair=pair, process=process,
exchange=exchange, data_handler=data_handler) timeframe=timeframe, datadir=datadir,
timerange=timerange, exchange=exchange, data_handler=data_handler)
def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optional[TimeRange], def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optional[TimeRange],
@ -153,13 +154,14 @@ def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optiona
return data, start_ms return data, start_ms
def _download_pair_history(datadir: Path, def _download_pair_history(pair: str, *,
datadir: Path,
exchange: Exchange, exchange: Exchange,
pair: str, *,
new_pairs_days: int = 30,
timeframe: str = '5m', timeframe: str = '5m',
timerange: Optional[TimeRange] = None, process: str = '',
data_handler: IDataHandler = None) -> bool: new_pairs_days: int = 30,
data_handler: IDataHandler = None,
timerange: Optional[TimeRange] = None) -> bool:
""" """
Download latest candles from the exchange for the pair and timeframe passed in parameters Download latest candles from the exchange for the pair and timeframe passed in parameters
The data is downloaded starting from the last correct data that The data is downloaded starting from the last correct data that
@ -177,7 +179,7 @@ def _download_pair_history(datadir: Path,
try: try:
logger.info( logger.info(
f'Download history data for pair: "{pair}", timeframe: {timeframe} ' f'Download history data for pair: "{pair}" ({process}), timeframe: {timeframe} '
f'and store in {datadir}.' f'and store in {datadir}.'
) )
@ -234,7 +236,7 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
""" """
pairs_not_available = [] pairs_not_available = []
data_handler = get_datahandler(datadir, data_format) data_handler = get_datahandler(datadir, data_format)
for pair in pairs: for idx, pair in enumerate(pairs, start=1):
if pair not in exchange.markets: if pair not in exchange.markets:
pairs_not_available.append(pair) pairs_not_available.append(pair)
logger.info(f"Skipping pair {pair}...") logger.info(f"Skipping pair {pair}...")
@ -247,10 +249,11 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
f'Deleting existing data for pair {pair}, interval {timeframe}.') f'Deleting existing data for pair {pair}, interval {timeframe}.')
logger.info(f'Downloading pair {pair}, interval {timeframe}.') logger.info(f'Downloading pair {pair}, interval {timeframe}.')
_download_pair_history(datadir=datadir, exchange=exchange, process = f'{idx}/{len(pairs)}'
pair=pair, timeframe=str(timeframe), _download_pair_history(pair=pair, process=process,
new_pairs_days=new_pairs_days, datadir=datadir, exchange=exchange,
timerange=timerange, data_handler=data_handler) timerange=timerange, data_handler=data_handler,
timeframe=str(timeframe), new_pairs_days=new_pairs_days)
return pairs_not_available return pairs_not_available

View File

@ -151,7 +151,7 @@ class Edge:
# Fake run-mode to Edge # Fake run-mode to Edge
prior_rm = self.config['runmode'] prior_rm = self.config['runmode']
self.config['runmode'] = RunMode.EDGE self.config['runmode'] = RunMode.EDGE
preprocessed = self.strategy.ohlcvdata_to_dataframe(data) preprocessed = self.strategy.advise_all_indicators(data)
self.config['runmode'] = prior_rm self.config['runmode'] = prior_rm
# Print timeframe # Print timeframe

View File

@ -15,6 +15,7 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
timeframe_to_seconds, validate_exchange, timeframe_to_seconds, validate_exchange,
validate_exchanges) validate_exchanges)
from freqtrade.exchange.ftx import Ftx from freqtrade.exchange.ftx import Ftx
from freqtrade.exchange.gateio import Gateio
from freqtrade.exchange.hitbtc import Hitbtc from freqtrade.exchange.hitbtc import Hitbtc
from freqtrade.exchange.kraken import Kraken from freqtrade.exchange.kraken import Kraken
from freqtrade.exchange.kucoin import Kucoin from freqtrade.exchange.kucoin import Kucoin

View File

@ -19,7 +19,8 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRU
decimal_to_precision) decimal_to_precision)
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import DEFAULT_AMOUNT_RESERVE_PERCENT, ListPairsWithTimeframes from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES,
ListPairsWithTimeframes)
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
from freqtrade.enums import Collateral from freqtrade.enums import Collateral
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
@ -366,9 +367,16 @@ class Exchange:
def validate_stakecurrency(self, stake_currency: str) -> None: def validate_stakecurrency(self, stake_currency: str) -> None:
""" """
Checks stake-currency against available currencies on the exchange. Checks stake-currency against available currencies on the exchange.
Only runs on startup. If markets have not been loaded, there's been a problem with
the connection to the exchange.
:param stake_currency: Stake-currency to validate :param stake_currency: Stake-currency to validate
:raise: OperationalException if stake-currency is not available. :raise: OperationalException if stake-currency is not available.
""" """
if not self._markets:
raise OperationalException(
'Could not load markets, therefore cannot start. '
'Please investigate the above error for more details.'
)
quote_currencies = self.get_quote_currencies() quote_currencies = self.get_quote_currencies()
if stake_currency not in quote_currencies: if stake_currency not in quote_currencies:
raise OperationalException( raise OperationalException(
@ -646,6 +654,8 @@ class Exchange:
if self.exchange_has('fetchL2OrderBook'): if self.exchange_has('fetchL2OrderBook'):
ob = self.fetch_l2_order_book(pair, 20) ob = self.fetch_l2_order_book(pair, 20)
ob_type = 'asks' if side == 'buy' else 'bids' ob_type = 'asks' if side == 'buy' else 'bids'
slippage = 0.05
max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage))
remaining_amount = amount remaining_amount = amount
filled_amount = 0 filled_amount = 0
@ -654,7 +664,9 @@ class Exchange:
book_entry_coin_volume = book_entry[1] book_entry_coin_volume = book_entry[1]
if remaining_amount > 0: if remaining_amount > 0:
if remaining_amount < book_entry_coin_volume: if remaining_amount < book_entry_coin_volume:
# Orderbook at this slot bigger than remaining amount
filled_amount += remaining_amount * book_entry_price filled_amount += remaining_amount * book_entry_price
break
else: else:
filled_amount += book_entry_coin_volume * book_entry_price filled_amount += book_entry_coin_volume * book_entry_price
remaining_amount -= book_entry_coin_volume remaining_amount -= book_entry_coin_volume
@ -663,7 +675,14 @@ class Exchange:
else: else:
# If remaining_amount wasn't consumed completely (break was not called) # If remaining_amount wasn't consumed completely (break was not called)
filled_amount += remaining_amount * book_entry_price filled_amount += remaining_amount * book_entry_price
forecast_avg_filled_price = filled_amount / amount forecast_avg_filled_price = max(filled_amount, 0) / amount
# Limit max. slippage to specified value
if side == 'buy':
forecast_avg_filled_price = min(forecast_avg_filled_price, max_slippage_val)
else:
forecast_avg_filled_price = max(forecast_avg_filled_price, max_slippage_val)
return self.price_to_precision(pair, forecast_avg_filled_price) return self.price_to_precision(pair, forecast_avg_filled_price)
return rate return rate
@ -822,7 +841,7 @@ class Exchange:
:param order: Order dict as returned from fetch_order() :param order: Order dict as returned from fetch_order()
:return: True if order has been cancelled without being filled, False otherwise. :return: True if order has been cancelled without being filled, False otherwise.
""" """
return (order.get('status') in ('closed', 'canceled', 'cancelled') return (order.get('status') in NON_OPEN_EXCHANGE_STATES
and order.get('filled') == 0.0) and order.get('filled') == 0.0)
@retrier @retrier
@ -1056,7 +1075,7 @@ class Exchange:
logger.debug(f"Using Last {conf_strategy['price_side'].capitalize()} / Last Price") logger.debug(f"Using Last {conf_strategy['price_side'].capitalize()} / Last Price")
ticker = self.fetch_ticker(pair) ticker = self.fetch_ticker(pair)
ticker_rate = ticker[conf_strategy['price_side']] ticker_rate = ticker[conf_strategy['price_side']]
if ticker['last']: if ticker['last'] and ticker_rate:
if side == 'buy' and ticker_rate > ticker['last']: if side == 'buy' and ticker_rate > ticker['last']:
balance = conf_strategy['ask_last_balance'] balance = conf_strategy['ask_last_balance']
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate) ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
@ -1271,7 +1290,7 @@ class Exchange:
logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list)) logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
input_coroutines = [] input_coroutines = []
cached_pairs = []
# Gather coroutines to run # Gather coroutines to run
for pair, timeframe in set(pair_list): for pair, timeframe in set(pair_list):
if (((pair, timeframe) not in self._klines) if (((pair, timeframe) not in self._klines)
@ -1283,6 +1302,7 @@ class Exchange:
"Using cached candle (OHLCV) data for pair %s, timeframe %s ...", "Using cached candle (OHLCV) data for pair %s, timeframe %s ...",
pair, timeframe pair, timeframe
) )
cached_pairs.append((pair, timeframe))
results = asyncio.get_event_loop().run_until_complete( results = asyncio.get_event_loop().run_until_complete(
asyncio.gather(*input_coroutines, return_exceptions=True)) asyncio.gather(*input_coroutines, return_exceptions=True))
@ -1305,6 +1325,10 @@ class Exchange:
results_df[(pair, timeframe)] = ohlcv_df results_df[(pair, timeframe)] = ohlcv_df
if cache: if cache:
self._klines[(pair, timeframe)] = ohlcv_df self._klines[(pair, timeframe)] = ohlcv_df
# Return cached klines
for pair, timeframe in cached_pairs:
results_df[(pair, timeframe)] = self.klines((pair, timeframe), copy=False)
return results_df return results_df
def _now_is_time_to_refresh(self, pair: str, timeframe: str) -> bool: def _now_is_time_to_refresh(self, pair: str, timeframe: str) -> bool:

View File

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

View File

@ -433,11 +433,11 @@ class FreqtradeBot(LoggingMixin):
if ((bid_check_dom.get('enabled', False)) and if ((bid_check_dom.get('enabled', False)) and
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)): (bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
if self._check_depth_of_market_buy(pair, bid_check_dom): if self._check_depth_of_market_buy(pair, bid_check_dom):
return self.execute_buy(pair, stake_amount, buy_tag=buy_tag) return self.execute_entry(pair, stake_amount, buy_tag=buy_tag)
else: else:
return False return False
return self.execute_buy(pair, stake_amount, buy_tag=buy_tag) return self.execute_entry(pair, stake_amount, buy_tag=buy_tag)
else: else:
return False return False
@ -465,8 +465,8 @@ class FreqtradeBot(LoggingMixin):
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
return False return False
def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None, def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None,
forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool: forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool:
""" """
Executes a limit buy for the given pair Executes a limit buy for the given pair
:param pair: pair for which we want to create a LIMIT_BUY :param pair: pair for which we want to create a LIMIT_BUY
@ -479,7 +479,13 @@ class FreqtradeBot(LoggingMixin):
buy_limit_requested = price buy_limit_requested = price
else: else:
# Calculate price # Calculate price
buy_limit_requested = self.exchange.get_rate(pair, refresh=True, side="buy") proposed_buy_rate = self.exchange.get_rate(pair, refresh=True, side="buy")
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=proposed_buy_rate)(
pair=pair, current_time=datetime.now(timezone.utc),
proposed_rate=proposed_buy_rate)
buy_limit_requested = self.get_valid_price(custom_entry_price, proposed_buy_rate)
if not buy_limit_requested: if not buy_limit_requested:
raise PricingError('Could not determine buy price.') raise PricingError('Could not determine buy price.')
@ -743,7 +749,7 @@ class FreqtradeBot(LoggingMixin):
trade.stoploss_order_id = None trade.stoploss_order_id = None
logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.error(f'Unable to place a stoploss order on exchange. {e}')
logger.warning('Selling the trade forcefully') logger.warning('Selling the trade forcefully')
self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple( self.execute_trade_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple(
sell_type=SellType.EMERGENCY_SELL)) sell_type=SellType.EMERGENCY_SELL))
except ExchangeError: except ExchangeError:
@ -861,7 +867,7 @@ class FreqtradeBot(LoggingMixin):
if should_sell.sell_flag: if should_sell.sell_flag:
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}') logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
self.execute_sell(trade, sell_rate, should_sell) self.execute_trade_exit(trade, sell_rate, should_sell)
return True return True
return False return False
@ -943,7 +949,7 @@ class FreqtradeBot(LoggingMixin):
was_trade_fully_canceled = False was_trade_fully_canceled = False
# Cancelled orders may have the status of 'canceled' or 'closed' # Cancelled orders may have the status of 'canceled' or 'closed'
if order['status'] not in ('cancelled', 'canceled', 'closed'): if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
filled_val = order.get('filled', 0.0) or 0.0 filled_val = order.get('filled', 0.0) or 0.0
filled_stake = filled_val * trade.open_rate filled_stake = filled_val * trade.open_rate
minstake = self.exchange.get_min_pair_stake_amount( minstake = self.exchange.get_min_pair_stake_amount(
@ -959,7 +965,7 @@ class FreqtradeBot(LoggingMixin):
# Avoid race condition where the order could not be cancelled coz its already filled. # Avoid race condition where the order could not be cancelled coz its already filled.
# Simply bailing here is the only safe way - as this order will then be # Simply bailing here is the only safe way - as this order will then be
# handled in the next iteration. # handled in the next iteration.
if corder.get('status') not in ('cancelled', 'canceled', 'closed'): if corder.get('status') not in constants.NON_OPEN_EXCHANGE_STATES:
logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.") logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.")
return False return False
else: else:
@ -981,7 +987,7 @@ class FreqtradeBot(LoggingMixin):
# if trade is partially complete, edit the stake details for the trade # if trade is partially complete, edit the stake details for the trade
# and close the order # and close the order
# cancel_order may not contain the full order dict, so we need to fallback # cancel_order may not contain the full order dict, so we need to fallback
# to the order dict aquired before cancelling. # to the order dict acquired before cancelling.
# we need to fall back to the values from order if corder does not contain these keys. # we need to fall back to the values from order if corder does not contain these keys.
trade.amount = filled_amount trade.amount = filled_amount
trade.stake_amount = trade.amount * trade.open_rate trade.stake_amount = trade.amount * trade.open_rate
@ -1062,9 +1068,9 @@ class FreqtradeBot(LoggingMixin):
raise DependencyException( raise DependencyException(
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}")
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool: def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool:
""" """
Executes a limit sell for the given trade and limit Executes a trade exit for the given trade and limit
:param trade: Trade instance :param trade: Trade instance
:param limit: limit rate for the sell order :param limit: limit rate for the sell order
:param sell_reason: Reason the sell was triggered :param sell_reason: Reason the sell was triggered
@ -1080,6 +1086,17 @@ class FreqtradeBot(LoggingMixin):
and self.strategy.order_types['stoploss_on_exchange']: and self.strategy.order_types['stoploss_on_exchange']:
limit = trade.stop_loss limit = trade.stop_loss
# set custom_exit_price if available
proposed_limit_rate = limit
current_profit = trade.calc_profit_ratio(limit)
custom_exit_price = strategy_safe_wrapper(self.strategy.custom_exit_price,
default_retval=proposed_limit_rate)(
pair=trade.pair, trade=trade,
current_time=datetime.now(timezone.utc),
proposed_rate=proposed_limit_rate, current_profit=current_profit)
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
# First cancelling stoploss on exchange ... # First cancelling stoploss on exchange ...
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
try: try:
@ -1129,7 +1146,7 @@ class FreqtradeBot(LoggingMixin):
trade.close_rate_requested = limit trade.close_rate_requested = limit
trade.sell_reason = sell_reason.sell_reason trade.sell_reason = sell_reason.sell_reason
# In case of market sell orders the order can be closed immediately # In case of market sell orders the order can be closed immediately
if order.get('status', 'unknown') == 'closed': if order.get('status', 'unknown') in ('closed', 'expired'):
self.update_trade_state(trade, trade.open_order_id, order) self.update_trade_state(trade, trade.open_order_id, order)
Trade.commit() Trade.commit()
@ -1368,7 +1385,9 @@ class FreqtradeBot(LoggingMixin):
if fee_currency: if fee_currency:
# fee_rate should use mean # fee_rate should use mean
fee_rate = sum(fee_rate_array) / float(len(fee_rate_array)) if fee_rate_array else None fee_rate = sum(fee_rate_array) / float(len(fee_rate_array)) if fee_rate_array else None
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) if fee_rate is not None and fee_rate < 0.02:
# Only update if fee-rate is < 2%
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC): if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
logger.warning(f"Amount {amount} does not match amount {trade.amount}") logger.warning(f"Amount {amount} does not match amount {trade.amount}")
@ -1379,3 +1398,26 @@ class FreqtradeBot(LoggingMixin):
amount=amount, fee_abs=fee_abs) amount=amount, fee_abs=fee_abs)
else: else:
return amount return amount
def get_valid_price(self, custom_price: float, proposed_price: float) -> float:
"""
Return the valid price.
Check if the custom price is of the good type if not return proposed_price
:return: valid price for the order
"""
if custom_price:
try:
valid_custom_price = float(custom_price)
except ValueError:
valid_custom_price = proposed_price
else:
valid_custom_price = proposed_price
cust_p_max_dist_r = self.config.get('custom_price_max_distance_ratio', 0.02)
min_custom_price_allowed = proposed_price - (proposed_price * cust_p_max_dist_r)
max_custom_price_allowed = proposed_price + (proposed_price * cust_p_max_dist_r)
# Bracket between min_custom_price_allowed and max_custom_price_allowed
return max(
min(valid_custom_price, max_custom_price_allowed),
min_custom_price_allowed)

View File

@ -86,6 +86,17 @@ class Backtesting:
"configuration or as cli argument `--timeframe 5m`") "configuration or as cli argument `--timeframe 5m`")
self.timeframe = str(self.config.get('timeframe')) self.timeframe = str(self.config.get('timeframe'))
self.timeframe_min = timeframe_to_minutes(self.timeframe) self.timeframe_min = timeframe_to_minutes(self.timeframe)
# Load detail timeframe if specified
self.timeframe_detail = str(self.config.get('timeframe_detail', ''))
if self.timeframe_detail:
self.timeframe_detail_min = timeframe_to_minutes(self.timeframe_detail)
if self.timeframe_min <= self.timeframe_detail_min:
raise OperationalException(
"Detail timeframe must be smaller than strategy timeframe.")
else:
self.timeframe_detail_min = 0
self.detail_data: Dict[str, DataFrame] = {}
self.pairlists = PairListManager(self.exchange, self.config) self.pairlists = PairListManager(self.exchange, self.config)
if 'VolumePairList' in self.pairlists.name_list: if 'VolumePairList' in self.pairlists.name_list:
@ -130,6 +141,9 @@ class Backtesting:
self.abort = False self.abort = False
def __del__(self): def __del__(self):
self.cleanup()
def cleanup(self):
LoggingMixin.show_output = True LoggingMixin.show_output = True
PairLocks.use_db = True PairLocks.use_db = True
Trade.use_db = True Trade.use_db = True
@ -185,6 +199,23 @@ class Backtesting:
self.progress.set_new_value(1) self.progress.set_new_value(1)
return data, self.timerange return data, self.timerange
def load_bt_data_detail(self) -> None:
"""
Loads backtest detail data (smaller timeframe) if necessary.
"""
if self.timeframe_detail:
self.detail_data = history.load_data(
datadir=self.config['datadir'],
pairs=self.pairlists.whitelist,
timeframe=self.timeframe_detail,
timerange=self.timerange,
startup_candles=0,
fail_without_data=True,
data_format=self.config.get('dataformat_ohlcv', 'json'),
)
else:
self.detail_data = {}
def prepare_backtest(self, enable_protections): def prepare_backtest(self, enable_protections):
""" """
Backtesting setup method - called once for every call to "backtest()". Backtesting setup method - called once for every call to "backtest()".
@ -215,7 +246,7 @@ class Backtesting:
""" """
# Every change to this headers list must evaluate further usages of the resulting tuple # Every change to this headers list must evaluate further usages of the resulting tuple
# and eventually change the constants for indexes at the top # and eventually change the constants for indexes at the top
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag']
data: Dict = {} data: Dict = {}
self.progress.init_step(BacktestState.CONVERT, len(processed)) self.progress.init_step(BacktestState.CONVERT, len(processed))
@ -223,13 +254,10 @@ class Backtesting:
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
self.check_abort() self.check_abort()
self.progress.increment() self.progress.increment()
has_buy_tag = 'buy_tag' in pair_data
headers = headers + ['buy_tag'] if has_buy_tag else headers
if not pair_data.empty: if not pair_data.empty:
pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist
pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist
if has_buy_tag: pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist
pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist
df_analyzed = self.strategy.advise_sell( df_analyzed = self.strategy.advise_sell(
self.strategy.advise_buy(pair_data, {'pair': pair}), self.strategy.advise_buy(pair_data, {'pair': pair}),
@ -242,14 +270,13 @@ class Backtesting:
# from the previous candle # from the previous candle
df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1) df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1)
df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1) df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1)
if has_buy_tag: df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1)
df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1)
df_analyzed.drop(df_analyzed.head(1).index, inplace=True)
# Update dataprovider cache # Update dataprovider cache
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
df_analyzed = df_analyzed.drop(df_analyzed.head(1).index)
# Convert from Pandas to list for performance reasons # Convert from Pandas to list for performance reasons
# (Looping Pandas is slow.) # (Looping Pandas is slow.)
data[pair] = df_analyzed[headers].values.tolist() data[pair] = df_analyzed[headers].values.tolist()
@ -321,15 +348,16 @@ class Backtesting:
else: else:
return sell_row[OPEN_IDX] return sell_row[OPEN_IDX]
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: def _get_sell_trade_entry_for_candle(self, trade: LocalTrade,
sell_row: Tuple) -> Optional[LocalTrade]:
sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore
sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX], sell_candle_time, sell_row[BUY_IDX],
sell_row[SELL_IDX], sell_row[SELL_IDX],
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
if sell.sell_flag: if sell.sell_flag:
trade.close_date = sell_row[DATE_IDX].to_pydatetime() trade.close_date = sell_candle_time
trade.sell_reason = sell.sell_reason trade.sell_reason = sell.sell_reason
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
@ -341,7 +369,7 @@ class Backtesting:
rate=closerate, rate=closerate,
time_in_force=time_in_force, time_in_force=time_in_force,
sell_reason=sell.sell_reason, sell_reason=sell.sell_reason,
current_time=sell_row[DATE_IDX].to_pydatetime()): current_time=sell_candle_time):
return None return None
trade.close(closerate, show_msg=False) trade.close(closerate, show_msg=False)
@ -349,6 +377,32 @@ class Backtesting:
return None return None
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
if self.timeframe_detail and trade.pair in self.detail_data:
sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
sell_candle_end = sell_candle_time + timedelta(minutes=self.timeframe_min)
detail_data = self.detail_data[trade.pair]
detail_data = detail_data.loc[
(detail_data['date'] >= sell_candle_time) &
(detail_data['date'] < sell_candle_end)
]
if len(detail_data) == 0:
# Fall back to "regular" data if no detail data was found for this candle
return self._get_sell_trade_entry_for_candle(trade, sell_row)
detail_data['buy'] = sell_row[BUY_IDX]
detail_data['sell'] = sell_row[SELL_IDX]
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
for det_row in detail_data[headers].values.tolist():
res = self._get_sell_trade_entry_for_candle(trade, det_row)
if res:
return res
return None
else:
return self._get_sell_trade_entry_for_candle(trade, sell_row)
def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]: def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]:
try: try:
stake_amount = self.wallets.get_trade_stake_amount(pair, None) stake_amount = self.wallets.get_trade_stake_amount(pair, None)
@ -465,6 +519,8 @@ class Backtesting:
for i, pair in enumerate(data): for i, pair in enumerate(data):
row_index = indexes[pair] row_index = indexes[pair]
try: try:
# Row is treated as "current incomplete candle".
# Buy / sell signals are shifted by 1 to compensate for this.
row = data[pair][row_index] row = data[pair][row_index]
except IndexError: except IndexError:
# missing Data for one pair at the end. # missing Data for one pair at the end.
@ -476,8 +532,8 @@ class Backtesting:
continue continue
row_index += 1 row_index += 1
self.dataprovider._set_dataframe_max_index(row_index)
indexes[pair] = row_index indexes[pair] = row_index
self.dataprovider._set_dataframe_max_index(row_index)
# without positionstacking, we can only have one open trade per pair. # without positionstacking, we can only have one open trade per pair.
# max_open_trades must be respected # max_open_trades must be respected
@ -501,7 +557,7 @@ class Backtesting:
open_trades[pair].append(trade) open_trades[pair].append(trade)
LocalTrade.add_bt_trade(trade) LocalTrade.add_bt_trade(trade)
for trade in open_trades[pair]: for trade in list(open_trades[pair]):
# also check the buying candle for sell conditions. # also check the buying candle for sell conditions.
trade_entry = self._get_sell_trade_entry(trade, row) trade_entry = self._get_sell_trade_entry(trade, row)
# Sell occurred # Sell occurred
@ -532,7 +588,8 @@ class Backtesting:
'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']),
} }
def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange): def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, DataFrame],
timerange: TimeRange):
self.progress.init_step(BacktestState.ANALYZE, 0) self.progress.init_step(BacktestState.ANALYZE, 0)
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
@ -551,7 +608,7 @@ class Backtesting:
max_open_trades = 0 max_open_trades = 0
# need to reprocess data every time to populate signals # need to reprocess data every time to populate signals
preprocessed = self.strategy.ohlcvdata_to_dataframe(data) preprocessed = self.strategy.advise_all_indicators(data)
# Trim startup period from analyzed dataframe # Trim startup period from analyzed dataframe
preprocessed_tmp = trim_dataframes(preprocessed, timerange, self.required_startup) preprocessed_tmp = trim_dataframes(preprocessed, timerange, self.required_startup)
@ -592,6 +649,7 @@ class Backtesting:
data: Dict[str, Any] = {} data: Dict[str, Any] = {}
data, timerange = self.load_bt_data() data, timerange = self.load_bt_data()
self.load_bt_data_detail()
logger.info("Dataload complete. Calculating indicators") logger.info("Dataload complete. Calculating indicators")
for strat in self.strategylist: for strat in self.strategylist:

View File

@ -107,13 +107,25 @@ class Hyperopt:
# Populate "fallback" functions here # Populate "fallback" functions here
# (hasattr is slow so should not be run during "regular" operations) # (hasattr is slow so should not be run during "regular" operations)
if hasattr(self.custom_hyperopt, 'populate_indicators'): if hasattr(self.custom_hyperopt, 'populate_indicators'):
self.backtesting.strategy.advise_indicators = ( # type: ignore logger.warning(
"DEPRECATED: Using `populate_indicators()` in the hyperopt file is deprecated. "
"Please move these methods to your strategy."
)
self.backtesting.strategy.populate_indicators = ( # type: ignore
self.custom_hyperopt.populate_indicators) # type: ignore self.custom_hyperopt.populate_indicators) # type: ignore
if hasattr(self.custom_hyperopt, 'populate_buy_trend'): if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
self.backtesting.strategy.advise_buy = ( # type: ignore logger.warning(
"DEPRECATED: Using `populate_buy_trend()` in the hyperopt file is deprecated. "
"Please move these methods to your strategy."
)
self.backtesting.strategy.populate_buy_trend = ( # type: ignore
self.custom_hyperopt.populate_buy_trend) # type: ignore self.custom_hyperopt.populate_buy_trend) # type: ignore
if hasattr(self.custom_hyperopt, 'populate_sell_trend'): if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
self.backtesting.strategy.advise_sell = ( # type: ignore logger.warning(
"DEPRECATED: Using `populate_sell_trend()` in the hyperopt file is deprecated. "
"Please move these methods to your strategy."
)
self.backtesting.strategy.populate_sell_trend = ( # type: ignore
self.custom_hyperopt.populate_sell_trend) # type: ignore self.custom_hyperopt.populate_sell_trend) # type: ignore
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
@ -394,7 +406,7 @@ class Hyperopt:
data, timerange = self.backtesting.load_bt_data() data, timerange = self.backtesting.load_bt_data()
logger.info("Dataload complete. Calculating indicators") logger.info("Dataload complete. Calculating indicators")
preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) preprocessed = self.backtesting.strategy.advise_all_indicators(data)
# Trim startup period from analyzed dataframe to get correct dates for output. # Trim startup period from analyzed dataframe to get correct dates for output.
processed = trim_dataframes(preprocessed, timerange, self.backtesting.required_startup) processed = trim_dataframes(preprocessed, timerange, self.backtesting.required_startup)

View File

@ -74,7 +74,7 @@ class HyperOptAuto(IHyperOpt):
return self._get_indicator_space('sell', 'sell_indicator_space') return self._get_indicator_space('sell', 'sell_indicator_space')
def protection_space(self) -> List['Dimension']: def protection_space(self) -> List['Dimension']:
return self._get_indicator_space('protection', 'indicator_space') return self._get_indicator_space('protection', 'protection_space')
def generate_roi_table(self, params: Dict) -> Dict[int, float]: def generate_roi_table(self, params: Dict) -> Dict[int, float]:
return self._get_func('generate_roi_table')(params) return self._get_func('generate_roi_table')(params)

View File

@ -0,0 +1,128 @@
import logging
from typing import List
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__)
def hyperopt_filter_epochs(epochs: List, filteroptions: dict, log: bool = True) -> List:
"""
Filter our items from the list of hyperopt results
"""
if filteroptions['only_best']:
epochs = [x for x in epochs if x['is_best']]
if filteroptions['only_profitable']:
epochs = [x for x in epochs
if x['results_metrics'].get('profit_total', 0) > 0]
epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions)
epochs = _hyperopt_filter_epochs_duration(epochs, filteroptions)
epochs = _hyperopt_filter_epochs_profit(epochs, filteroptions)
epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions)
if log:
logger.info(f"{len(epochs)} " +
("best " if filteroptions['only_best'] else "") +
("profitable " if filteroptions['only_profitable'] else "") +
"epochs found.")
return epochs
def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int):
"""
Filter epochs with trade-counts > trades
"""
return [
x for x in epochs if x['results_metrics'].get('total_trades', 0) > trade_count
]
def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List:
if filteroptions['filter_min_trades'] > 0:
epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades'])
if filteroptions['filter_max_trades'] > 0:
epochs = [
x for x in epochs
if x['results_metrics'].get('total_trades') < filteroptions['filter_max_trades']
]
return epochs
def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List:
def get_duration_value(x):
# Duration in minutes ...
if 'holding_avg_s' in x['results_metrics']:
avg = x['results_metrics']['holding_avg_s']
return avg // 60
raise OperationalException(
"Holding-average not available. Please omit the filter on average time, "
"or rerun hyperopt with this version")
if filteroptions['filter_min_avg_time'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if get_duration_value(x) > filteroptions['filter_min_avg_time']
]
if filteroptions['filter_max_avg_time'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if get_duration_value(x) < filteroptions['filter_max_avg_time']
]
return epochs
def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
if filteroptions['filter_min_avg_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics'].get('profit_mean', 0) * 100
> filteroptions['filter_min_avg_profit']
]
if filteroptions['filter_max_avg_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics'].get('profit_mean', 0) * 100
< filteroptions['filter_max_avg_profit']
]
if filteroptions['filter_min_total_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics'].get('profit_total_abs', 0)
> filteroptions['filter_min_total_profit']
]
if filteroptions['filter_max_total_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics'].get('profit_total_abs', 0)
< filteroptions['filter_max_total_profit']
]
return epochs
def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List:
if filteroptions['filter_min_objective'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']]
if filteroptions['filter_max_objective'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']]
return epochs

View File

@ -4,7 +4,7 @@ import logging
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, Iterator, List, Optional, Tuple
import numpy as np import numpy as np
import rapidjson import rapidjson
@ -15,6 +15,7 @@ from pandas import isna, json_normalize
from freqtrade.constants import FTHYPT_FILEVERSION, USERPATH_STRATEGIES from freqtrade.constants import FTHYPT_FILEVERSION, USERPATH_STRATEGIES
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import deep_merge_dicts, round_coin_value, round_dict, safe_value_fallback2 from freqtrade.misc import deep_merge_dicts, round_coin_value, round_dict, safe_value_fallback2
from freqtrade.optimize.hyperopt_epoch_filters import hyperopt_filter_epochs
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -89,46 +90,70 @@ class HyperoptTools():
return any(s in config['spaces'] for s in [space, 'all', 'default']) return any(s in config['spaces'] for s in [space, 'all', 'default'])
@staticmethod @staticmethod
def _read_results_pickle(results_file: Path) -> List: def _read_results(results_file: Path, batch_size: int = 10) -> Iterator[List[Any]]:
""" """
Read hyperopt results from pickle file Stream hyperopt results from file
LEGACY method - new files are written as json and cannot be read with this method.
"""
from joblib import load
logger.info(f"Reading pickled epochs from '{results_file}'")
data = load(results_file)
return data
@staticmethod
def _read_results(results_file: Path) -> List:
"""
Read hyperopt results from file
""" """
import rapidjson import rapidjson
logger.info(f"Reading epochs from '{results_file}'") logger.info(f"Reading epochs from '{results_file}'")
with results_file.open('r') as f: with results_file.open('r') as f:
data = [rapidjson.loads(line) for line in f] data = []
return data for line in f:
data += [rapidjson.loads(line)]
if len(data) >= batch_size:
yield data
data = []
yield data
@staticmethod @staticmethod
def load_previous_results(results_file: Path) -> List: def _test_hyperopt_results_exist(results_file) -> bool:
"""
Load data for epochs from the file if we have one
"""
epochs: List = []
if results_file.is_file() and results_file.stat().st_size > 0: if results_file.is_file() and results_file.stat().st_size > 0:
if results_file.suffix == '.pickle': if results_file.suffix == '.pickle':
epochs = HyperoptTools._read_results_pickle(results_file) raise OperationalException(
else: "Legacy hyperopt results are no longer supported."
epochs = HyperoptTools._read_results(results_file) "Please rerun hyperopt or use an older version to load this file."
# Detection of some old format, without 'is_best' field saved )
if epochs[0].get('is_best') is None: return True
else:
# No file found.
return False
@staticmethod
def load_filtered_results(results_file: Path, config: Dict[str, Any]) -> Tuple[List, int]:
filteroptions = {
'only_best': config.get('hyperopt_list_best', False),
'only_profitable': config.get('hyperopt_list_profitable', False),
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
'filter_max_objective': config.get('hyperopt_list_max_objective', None),
}
if not HyperoptTools._test_hyperopt_results_exist(results_file):
# No file found.
return [], 0
epochs = []
total_epochs = 0
for epochs_tmp in HyperoptTools._read_results(results_file):
if total_epochs == 0 and epochs_tmp[0].get('is_best') is None:
raise OperationalException( raise OperationalException(
"The file with HyperoptTools results is incompatible with this version " "The file with HyperoptTools results is incompatible with this version "
"of Freqtrade and cannot be loaded.") "of Freqtrade and cannot be loaded.")
logger.info(f"Loaded {len(epochs)} previous evaluations from disk.") total_epochs += len(epochs_tmp)
return epochs epochs += hyperopt_filter_epochs(epochs_tmp, filteroptions, log=False)
logger.info(f"Loaded {total_epochs} previous evaluations from disk.")
# Final filter run ...
epochs = hyperopt_filter_epochs(epochs, filteroptions, log=True)
return epochs, total_epochs
@staticmethod @staticmethod
def show_epoch_details(results, total_epochs: int, print_json: bool, def show_epoch_details(results, total_epochs: int, print_json: bool,
@ -433,21 +458,14 @@ class HyperoptTools():
trials['Best'] = '' trials['Best'] = ''
trials['Stake currency'] = config['stake_currency'] trials['Stake currency'] = config['stake_currency']
if 'results_metrics.total_trades' in trials: base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades',
base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades', 'results_metrics.profit_mean', 'results_metrics.profit_median',
'results_metrics.profit_mean', 'results_metrics.profit_median', 'results_metrics.profit_total',
'results_metrics.profit_total', 'Stake currency',
'Stake currency', 'results_metrics.profit_total_abs', 'results_metrics.holding_avg',
'results_metrics.profit_total_abs', 'results_metrics.holding_avg', 'loss', 'is_initial_point', 'is_best']
'loss', 'is_initial_point', 'is_best'] perc_multi = 100
perc_multi = 100
else:
perc_multi = 1
base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.avg_profit', 'results_metrics.median_profit',
'results_metrics.total_profit',
'Stake currency', 'results_metrics.profit', 'results_metrics.duration',
'loss', 'is_initial_point', 'is_best']
param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()] param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()]
trials = trials[base_metrics + param_metrics] trials = trials[base_metrics + param_metrics]
@ -475,11 +493,6 @@ class HyperoptTools():
trials['Avg profit'] = trials['Avg profit'].apply( trials['Avg profit'] = trials['Avg profit'].apply(
lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else "" lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else ""
) )
if perc_multi == 1:
trials['Avg duration'] = trials['Avg duration'].apply(
lambda x: f'{x:,.1f} m' if isinstance(
x, float) else f"{x.total_seconds() // 60:,.1f} m" if not isna(x) else ""
)
trials['Objective'] = trials['Objective'].apply( trials['Objective'] = trials['Objective'].apply(
lambda x: f'{x:,.5f}' if x != 100000 else "" lambda x: f'{x:,.5f}' if x != 100000 else ""
) )

View File

@ -368,6 +368,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
'max_open_trades_setting': (config['max_open_trades'] 'max_open_trades_setting': (config['max_open_trades']
if config['max_open_trades'] != float('inf') else -1), if config['max_open_trades'] != float('inf') else -1),
'timeframe': config['timeframe'], 'timeframe': config['timeframe'],
'timeframe_detail': config.get('timeframe_detail', ''),
'timerange': config.get('timerange', ''), 'timerange': config.get('timerange', ''),
'enable_protections': config.get('enable_protections', False), 'enable_protections': config.get('enable_protections', False),
'strategy_name': strategy, 'strategy_name': strategy,

View File

@ -13,7 +13,7 @@ from sqlalchemy.orm import Query, declarative_base, relationship, scoped_session
from sqlalchemy.pool import StaticPool from sqlalchemy.pool import StaticPool
from sqlalchemy.sql.schema import UniqueConstraint from sqlalchemy.sql.schema import UniqueConstraint
from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES
from freqtrade.enums import SellType from freqtrade.enums import SellType
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.leverage import interest from freqtrade.leverage import interest
@ -164,9 +164,9 @@ class Order(_DECL_BASE):
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc) self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
self.ft_is_open = True self.ft_is_open = True
if self.status in ('closed', 'canceled', 'cancelled'): if self.status in NON_OPEN_EXCHANGE_STATES:
self.ft_is_open = False self.ft_is_open = False
if order.get('filled', 0) > 0: if (order.get('filled', 0.0) or 0.0) > 0:
self.order_filled_date = datetime.now(timezone.utc) self.order_filled_date = datetime.now(timezone.utc)
self.order_update_date = datetime.now(timezone.utc) self.order_update_date = datetime.now(timezone.utc)
@ -451,12 +451,12 @@ class LocalTrade():
LocalTrade.trades_open = [] LocalTrade.trades_open = []
LocalTrade.total_profit = 0 LocalTrade.total_profit = 0
def adjust_min_max_rates(self, current_price: float) -> None: def adjust_min_max_rates(self, current_price: float, current_price_low: float) -> None:
""" """
Adjust the max_rate and min_rate. Adjust the max_rate and min_rate.
""" """
self.max_rate = max(current_price, self.max_rate or self.open_rate) self.max_rate = max(current_price, self.max_rate or self.open_rate)
self.min_rate = min(current_price, self.min_rate or self.open_rate) self.min_rate = min(current_price_low, self.min_rate or self.open_rate)
def adjust_stop_loss(self, current_price: float, stoploss: float, def adjust_stop_loss(self, current_price: float, stoploss: float,
initial: bool = False) -> None: initial: bool = False) -> None:

View File

@ -538,7 +538,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
- Initializes plot-script - Initializes plot-script
- Get candle (OHLCV) data - Get candle (OHLCV) data
- Generate Dafaframes populated with indicators and signals based on configured strategy - Generate Dafaframes populated with indicators and signals based on configured strategy
- Load trades excecuted during the selected period - Load trades executed during the selected period
- Generate Plotly plot objects - Generate Plotly plot objects
- Generate plot files - Generate plot files
:return: None :return: None

View File

@ -150,18 +150,20 @@ class IPairList(LoggingMixin, ABC):
for pair in pairlist: for pair in pairlist:
# pair is not in the generated dynamic market or has the wrong stake currency # pair is not in the generated dynamic market or has the wrong stake currency
if pair not in markets: if pair not in markets:
logger.warning(f"Pair {pair} is not compatible with exchange " self.log_once(f"Pair {pair} is not compatible with exchange "
f"{self._exchange.name}. Removing it from whitelist..") f"{self._exchange.name}. Removing it from whitelist..",
logger.warning)
continue continue
if not self._exchange.market_is_tradable(markets[pair]): if not self._exchange.market_is_tradable(markets[pair]):
logger.warning(f"Pair {pair} is not tradable with Freqtrade." self.log_once(f"Pair {pair} is not tradable with Freqtrade."
"Removing it from whitelist..") "Removing it from whitelist..", logger.warning)
continue continue
if self._exchange.get_pair_quote_currency(pair) != self._config['stake_currency']: if self._exchange.get_pair_quote_currency(pair) != self._config['stake_currency']:
logger.warning(f"Pair {pair} is not compatible with your stake currency " self.log_once(f"Pair {pair} is not compatible with your stake currency "
f"{self._config['stake_currency']}. Removing it from whitelist..") f"{self._config['stake_currency']}. Removing it from whitelist..",
logger.warning)
continue continue
# Check if market is active # Check if market is active

View File

@ -4,6 +4,7 @@ Volume PairList provider
Provides dynamic pair list based on trade volumes Provides dynamic pair list based on trade volumes
""" """
import logging import logging
from functools import partial
from typing import Any, Dict, List from typing import Any, Dict, List
import arrow import arrow
@ -115,7 +116,7 @@ class VolumePairList(IPairList):
pairlist = self._pair_cache.get('pairlist') pairlist = self._pair_cache.get('pairlist')
if pairlist: if pairlist:
# Item found - no refresh necessary # Item found - no refresh necessary
return pairlist return pairlist.copy()
else: else:
# Use fresh pairlist # Use fresh pairlist
# Check if pair quote currency equals to the stake currency. # Check if pair quote currency equals to the stake currency.
@ -126,7 +127,7 @@ class VolumePairList(IPairList):
pairlist = [s['symbol'] for s in filtered_tickers] pairlist = [s['symbol'] for s in filtered_tickers]
pairlist = self.filter_pairlist(pairlist, tickers) pairlist = self.filter_pairlist(pairlist, tickers)
self._pair_cache['pairlist'] = pairlist self._pair_cache['pairlist'] = pairlist.copy()
return pairlist return pairlist
@ -203,7 +204,7 @@ class VolumePairList(IPairList):
# Validate whitelist to only have active market pairs # Validate whitelist to only have active market pairs
pairs = self._whitelist_for_active_markets([s['symbol'] for s in sorted_tickers]) pairs = self._whitelist_for_active_markets([s['symbol'] for s in sorted_tickers])
pairs = self.verify_blacklist(pairs, logger.info) pairs = self.verify_blacklist(pairs, partial(self.log_once, logmethod=logger.info))
# Limit pairlist to the requested number of pairs # Limit pairlist to the requested number of pairs
pairs = pairs[:self._number_pairs] pairs = pairs[:self._number_pairs]

View File

@ -120,5 +120,6 @@ class RangeStabilityFilter(IPairList):
logger.info) logger.info)
result = False result = False
self._pair_cache[pair] = result self._pair_cache[pair] = result
else:
self.log_once(f"Removed {pair} from whitelist, no candles found.", logger.info)
return result return result

View File

@ -46,11 +46,14 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
if ( if (
not ApiServer._bt not ApiServer._bt
or lastconfig.get('timeframe') != strat.timeframe or lastconfig.get('timeframe') != strat.timeframe
or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail')
or lastconfig.get('dry_run_wallet') != btconfig.get('dry_run_wallet', 0) or lastconfig.get('dry_run_wallet') != btconfig.get('dry_run_wallet', 0)
or lastconfig.get('timerange') != btconfig['timerange'] or lastconfig.get('timerange') != btconfig['timerange']
): ):
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
ApiServer._bt = Backtesting(btconfig) ApiServer._bt = Backtesting(btconfig)
if ApiServer._bt.timeframe_detail:
ApiServer._bt.load_bt_data_detail()
# Only reload data if timeframe changed. # Only reload data if timeframe changed.
if ( if (

View File

@ -324,6 +324,7 @@ class PairHistory(BaseModel):
class BacktestRequest(BaseModel): class BacktestRequest(BaseModel):
strategy: str strategy: str
timeframe: Optional[str] timeframe: Optional[str]
timeframe_detail: Optional[str]
timerange: Optional[str] timerange: Optional[str]
max_open_trades: Optional[int] max_open_trades: Optional[int]
stake_amount: Optional[Union[float, str]] stake_amount: Optional[Union[float, str]]

View File

@ -223,11 +223,11 @@ def list_strategies(config=Depends(get_config)):
@router.get('/strategy/{strategy}', response_model=StrategyResponse, tags=['strategy']) @router.get('/strategy/{strategy}', response_model=StrategyResponse, tags=['strategy'])
def get_strategy(strategy: str, config=Depends(get_config)): def get_strategy(strategy: str, config=Depends(get_config)):
config = deepcopy(config) config_ = deepcopy(config)
from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.resolvers.strategy_resolver import StrategyResolver
try: try:
strategy_obj = StrategyResolver._load_strategy(strategy, config, strategy_obj = StrategyResolver._load_strategy(strategy, config_,
extra_dir=config.get('strategy_path')) extra_dir=config_.get('strategy_path'))
except OperationalException: except OperationalException:
raise HTTPException(status_code=404, detail='Strategy not found') raise HTTPException(status_code=404, detail='Strategy not found')

View File

@ -32,8 +32,11 @@ class UvicornServer(uvicorn.Server):
asyncio_setup() asyncio_setup()
else: else:
asyncio.set_event_loop(uvloop.new_event_loop()) asyncio.set_event_loop(uvloop.new_event_loop())
try:
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
except RuntimeError:
# When running in a thread, we'll not have an eventloop yet.
loop = asyncio.new_event_loop()
loop.run_until_complete(self.serve(sockets=sockets)) loop.run_until_complete(self.serve(sockets=sockets))
@contextlib.contextmanager @contextlib.contextmanager

View File

@ -29,6 +29,16 @@ async def ui_version():
} }
def is_relative_to(path, base) -> bool:
# Helper function simulating behaviour of is_relative_to, which was only added in python 3.9
try:
path.relative_to(base)
return True
except ValueError:
pass
return False
@router_ui.get('/{rest_of_path:path}', include_in_schema=False) @router_ui.get('/{rest_of_path:path}', include_in_schema=False)
async def index_html(rest_of_path: str): async def index_html(rest_of_path: str):
""" """
@ -37,8 +47,11 @@ async def index_html(rest_of_path: str):
if rest_of_path.startswith('api') or rest_of_path.startswith('.'): if rest_of_path.startswith('api') or rest_of_path.startswith('.'):
raise HTTPException(status_code=404, detail="Not Found") raise HTTPException(status_code=404, detail="Not Found")
uibase = Path(__file__).parent / 'ui/installed/' uibase = Path(__file__).parent / 'ui/installed/'
if (uibase / rest_of_path).is_file(): filename = uibase / rest_of_path
return FileResponse(str(uibase / rest_of_path)) # It's security relevant to check "relative_to".
# Without this, Directory-traversal is possible.
if filename.is_file() and is_relative_to(filename, uibase):
return FileResponse(str(filename))
index_file = uibase / 'index.html' index_file = uibase / 'index.html'
if not index_file.is_file(): if not index_file.is_file():

View File

@ -5,7 +5,7 @@ e.g BTC to USD
import datetime import datetime
import logging import logging
from typing import Dict from typing import Dict, List
from cachetools.ttl import TTLCache from cachetools.ttl import TTLCache
from pycoingecko import CoinGeckoAPI from pycoingecko import CoinGeckoAPI
@ -25,8 +25,7 @@ class CryptoToFiatConverter:
""" """
__instance = None __instance = None
_coingekko: CoinGeckoAPI = None _coingekko: CoinGeckoAPI = None
_coinlistings: List[Dict] = []
_cryptomap: Dict = {}
_backoff: float = 0.0 _backoff: float = 0.0
def __new__(cls): def __new__(cls):
@ -49,9 +48,8 @@ class CryptoToFiatConverter:
def _load_cryptomap(self) -> None: def _load_cryptomap(self) -> None:
try: try:
coinlistings = self._coingekko.get_coins_list() # Use list-comprehension to ensure we get a list.
# Create mapping table from symbol to coingekko_id self._coinlistings = [x for x in self._coingekko.get_coins_list()]
self._cryptomap = {x['symbol']: x['id'] for x in coinlistings}
except RequestException as request_exception: except RequestException as request_exception:
if "429" in str(request_exception): if "429" in str(request_exception):
logger.warning( logger.warning(
@ -69,6 +67,24 @@ class CryptoToFiatConverter:
logger.error( logger.error(
f"Could not load FIAT Cryptocurrency map for the following problem: {exception}") f"Could not load FIAT Cryptocurrency map for the following problem: {exception}")
def _get_gekko_id(self, crypto_symbol):
if not self._coinlistings:
if self._backoff <= datetime.datetime.now().timestamp():
self._load_cryptomap()
# Still not loaded.
if not self._coinlistings:
return None
else:
return None
found = [x for x in self._coinlistings if x['symbol'] == crypto_symbol]
if len(found) == 1:
return found[0]['id']
if len(found) > 0:
# Wrong!
logger.warning(f"Found multiple mappings in goingekko for {crypto_symbol}.")
return None
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float: def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
""" """
Convert an amount of crypto-currency to fiat Convert an amount of crypto-currency to fiat
@ -143,22 +159,14 @@ class CryptoToFiatConverter:
if crypto_symbol == fiat_symbol: if crypto_symbol == fiat_symbol:
return 1.0 return 1.0
if self._cryptomap == {}: _gekko_id = self._get_gekko_id(crypto_symbol)
if self._backoff <= datetime.datetime.now().timestamp():
self._load_cryptomap()
# return 0.0 if we still don't have data to check, no reason to proceed
if self._cryptomap == {}:
return 0.0
else:
return 0.0
if crypto_symbol not in self._cryptomap: if not _gekko_id:
# return 0 for unsupported stake currencies (fiat-convert should not break the bot) # return 0 for unsupported stake currencies (fiat-convert should not break the bot)
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol) logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
return 0.0 return 0.0
try: try:
_gekko_id = self._cryptomap[crypto_symbol]
return float( return float(
self._coingekko.get_price( self._coingekko.get_price(
ids=_gekko_id, ids=_gekko_id,

View File

@ -557,7 +557,7 @@ class RPC:
current_rate = self._freqtrade.exchange.get_rate( current_rate = self._freqtrade.exchange.get_rate(
trade.pair, refresh=False, side="sell") trade.pair, refresh=False, side="sell")
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
self._freqtrade.execute_sell(trade, current_rate, sell_reason) self._freqtrade.execute_trade_exit(trade, current_rate, sell_reason)
# ---- EOF def _exec_forcesell ---- # ---- EOF def _exec_forcesell ----
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
@ -613,7 +613,7 @@ class RPC:
stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair) stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair)
# execute buy # execute buy
if self._freqtrade.execute_buy(pair, stakeamount, price, forcebuy=True): if self._freqtrade.execute_entry(pair, stakeamount, price, forcebuy=True):
Trade.commit() Trade.commit()
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
return trade return trade
@ -776,7 +776,7 @@ class RPC:
if has_content: if has_content:
dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000 dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000
# Move open to seperate column when signal for easy plotting # Move open to separate column when signal for easy plotting
if 'buy' in dataframe.columns: if 'buy' in dataframe.columns:
buy_mask = (dataframe['buy'] == 1) buy_mask = (dataframe['buy'] == 1)
buy_signals = int(buy_mask.sum()) buy_signals = int(buy_mask.sum())

View File

@ -120,6 +120,8 @@ class IStrategy(ABC, HyperStrategyMixin):
# and wallets - access to the current balance. # and wallets - access to the current balance.
dp: Optional[DataProvider] = None dp: Optional[DataProvider] = None
wallets: Optional[Wallets] = None wallets: Optional[Wallets] = None
# Filled from configuration
stake_currency: str
# container variable for strategy source code # container variable for strategy source code
__source__: str = '' __source__: str = ''
@ -280,6 +282,43 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
return self.stoploss return self.stoploss
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
**kwargs) -> float:
"""
Custom entry price logic, returning the new entry price.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns None, orderbook is used to set entry price
:param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime
:param proposed_rate: Rate, calculated based on pricing settings in ask_strategy.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New entry price value if provided
"""
return proposed_rate
def custom_exit_price(self, pair: str, trade: Trade,
current_time: datetime, proposed_rate: float,
current_profit: float, **kwargs) -> float:
"""
Custom exit price logic, returning the new exit price.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns None, orderbook is used to set exit price
:param pair: Pair that's currently analyzed
:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param proposed_rate: Rate, calculated based on pricing settings in ask_strategy.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New exit price value if provided
"""
return proposed_rate
def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs) -> Optional[Union[str, bool]]: current_profit: float, **kwargs) -> Optional[Union[str, bool]]:
""" """
@ -577,7 +616,7 @@ class IStrategy(ABC, HyperStrategyMixin):
current_rate = rate current_rate = rate
current_profit = trade.calc_profit_ratio(current_rate) current_profit = trade.calc_profit_ratio(current_rate)
trade.adjust_min_max_rates(high or current_rate) trade.adjust_min_max_rates(high or current_rate, low or current_rate)
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
current_time=date, current_profit=current_profit, current_time=date, current_profit=current_profit,
@ -741,7 +780,7 @@ class IStrategy(ABC, HyperStrategyMixin):
else: else:
return current_profit > roi return current_profit > roi
def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: def advise_all_indicators(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
""" """
Populates indicators for given candle (OHLCV) data (for multiple pairs) Populates indicators for given candle (OHLCV) data (for multiple pairs)
Does not run advise_buy or advise_sell! Does not run advise_buy or advise_sell!

View File

@ -25,7 +25,7 @@
"ask_strategy": { "ask_strategy": {
"price_side": "ask", "price_side": "ask",
"use_order_book": true, "use_order_book": true,
"order_book_top": 1, "order_book_top": 1
}, },
{{ exchange | indent(4) }}, {{ exchange | indent(4) }},
"pairlists": [ "pairlists": [

View File

@ -36,6 +36,6 @@
"BNB/TUSD", "BNB/TUSD",
"BNB/USDC", "BNB/USDC",
"BNB/USDS", "BNB/USDS",
"BNB/USDT", "BNB/USDT"
] ]
} }

View File

@ -6,7 +6,7 @@
coveralls==3.2.0 coveralls==3.2.0
flake8==3.9.2 flake8==3.9.2
flake8-type-annotations==0.1.0 flake8-type-annotations==0.1.0
flake8-tidy-imports==4.3.0 flake8-tidy-imports==4.4.1
mypy==0.910 mypy==0.910
pytest==6.2.4 pytest==6.2.4
pytest-asyncio==0.15.1 pytest-asyncio==0.15.1
@ -19,7 +19,7 @@ isort==5.9.3
nbconvert==6.1.0 nbconvert==6.1.0
# mypy types # mypy types
types-cachetools==0.1.9 types-cachetools==4.2.0
types-filelock==0.1.4 types-filelock==0.1.5
types-requests==2.25.1 types-requests==2.25.6
types-tabulate==0.1.1 types-tabulate==0.8.2

View File

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

View File

@ -1,11 +1,11 @@
numpy==1.21.1 numpy==1.21.2
pandas==1.3.1 pandas==1.3.2
ccxt==1.54.24 ccxt==1.55.56
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==3.4.7 cryptography==3.4.8
aiohttp==3.7.4.post0 aiohttp==3.7.4.post0
SQLAlchemy==1.4.22 SQLAlchemy==1.4.23
python-telegram-bot==13.7 python-telegram-bot==13.7
arrow==1.1.1 arrow==1.1.1
cachetools==4.2.2 cachetools==4.2.2
@ -31,8 +31,8 @@ python-rapidjson==1.4
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.68.0 fastapi==0.68.1
uvicorn==0.14.0 uvicorn==0.15.0
pyjwt==2.1.0 pyjwt==2.1.0
aiofiles==0.7.0 aiofiles==0.7.0
@ -40,4 +40,4 @@ aiofiles==0.7.0
colorama==0.4.4 colorama==0.4.4
# Building config files interactively # Building config files interactively
questionary==1.10.0 questionary==1.10.0
prompt-toolkit==3.0.19 prompt-toolkit==3.0.20

View File

@ -119,6 +119,7 @@ function install_mac_newer_python_dependencies() {
echo "-------------------------" echo "-------------------------"
brew install hdf5 brew install hdf5
fi fi
export HDF5_DIR=$(brew --prefix)
if [ ! $(brew --prefix --installed c-blosc 2>/dev/null) ] if [ ! $(brew --prefix --installed c-blosc 2>/dev/null) ]
then then
@ -127,6 +128,7 @@ function install_mac_newer_python_dependencies() {
echo "-------------------------" echo "-------------------------"
brew install c-blosc brew install c-blosc
fi fi
export CBLOSC_DIR=$(brew --prefix)
} }
# Install bot MacOS # Install bot MacOS
@ -163,7 +165,7 @@ function update() {
# Reset Develop or Stable branch # Reset Develop or Stable branch
function reset() { function reset() {
echo "----------------------------" echo "----------------------------"
echo "Reseting branch and virtual env" echo "Resetting branch and virtual env"
echo "----------------------------" echo "----------------------------"
if [ "1" == $(git branch -vv |grep -cE "\* develop|\* stable") ] if [ "1" == $(git branch -vv |grep -cE "\* develop|\* stable") ]

View File

@ -510,17 +510,6 @@ def test_start_new_strategy(mocker, caplog):
start_new_strategy(get_args(args)) start_new_strategy(get_args(args))
def test_start_new_strategy_DefaultStrat(mocker, caplog):
args = [
"new-strategy",
"--strategy",
"DefaultStrategy"
]
with pytest.raises(OperationalException,
match=r"DefaultStrategy is not allowed as name\."):
start_new_strategy(get_args(args))
def test_start_new_strategy_no_arg(mocker, caplog): def test_start_new_strategy_no_arg(mocker, caplog):
args = [ args = [
"new-strategy", "new-strategy",
@ -552,17 +541,6 @@ def test_start_new_hyperopt(mocker, caplog):
start_new_hyperopt(get_args(args)) start_new_hyperopt(get_args(args))
def test_start_new_hyperopt_DefaultHyperopt(mocker, caplog):
args = [
"new-hyperopt",
"--hyperopt",
"DefaultHyperopt"
]
with pytest.raises(OperationalException,
match=r"DefaultHyperopt is not allowed as name\."):
start_new_hyperopt(get_args(args))
def test_start_new_hyperopt_no_arg(mocker): def test_start_new_hyperopt_no_arg(mocker):
args = [ args = [
"new-hyperopt", "new-hyperopt",
@ -827,9 +805,9 @@ def test_start_list_strategies(mocker, caplog, capsys):
# pargs['config'] = None # pargs['config'] = None
start_list_strategies(pargs) start_list_strategies(pargs)
captured = capsys.readouterr() captured = capsys.readouterr()
assert "TestStrategyLegacy" in captured.out assert "TestStrategyLegacyV1" in captured.out
assert "legacy_strategy.py" not in captured.out assert "legacy_strategy_v1.py" not in captured.out
assert "DefaultStrategy" in captured.out assert "StrategyTestV2" in captured.out
# Test regular output # Test regular output
args = [ args = [
@ -842,9 +820,9 @@ def test_start_list_strategies(mocker, caplog, capsys):
# pargs['config'] = None # pargs['config'] = None
start_list_strategies(pargs) start_list_strategies(pargs)
captured = capsys.readouterr() captured = capsys.readouterr()
assert "TestStrategyLegacy" in captured.out assert "TestStrategyLegacyV1" in captured.out
assert "legacy_strategy.py" in captured.out assert "legacy_strategy_v1.py" in captured.out
assert "DefaultStrategy" in captured.out assert "StrategyTestV2" in captured.out
def test_start_list_hyperopts(mocker, caplog, capsys): def test_start_list_hyperopts(mocker, caplog, capsys):
@ -861,7 +839,7 @@ def test_start_list_hyperopts(mocker, caplog, capsys):
captured = capsys.readouterr() captured = capsys.readouterr()
assert "TestHyperoptLegacy" not in captured.out assert "TestHyperoptLegacy" not in captured.out
assert "legacy_hyperopt.py" not in captured.out assert "legacy_hyperopt.py" not in captured.out
assert "DefaultHyperOpt" in captured.out assert "HyperoptTestSepFile" in captured.out
assert "test_hyperopt.py" not in captured.out assert "test_hyperopt.py" not in captured.out
# Test regular output # Test regular output
@ -876,7 +854,7 @@ def test_start_list_hyperopts(mocker, caplog, capsys):
captured = capsys.readouterr() captured = capsys.readouterr()
assert "TestHyperoptLegacy" not in captured.out assert "TestHyperoptLegacy" not in captured.out
assert "legacy_hyperopt.py" not in captured.out assert "legacy_hyperopt.py" not in captured.out
assert "DefaultHyperOpt" in captured.out assert "HyperoptTestSepFile" in captured.out
def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
@ -938,247 +916,261 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
pytest.fail(f'Expected well formed JSON, but failed to parse: {captured.out}') pytest.fail(f'Expected well formed JSON, but failed to parse: {captured.out}')
def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmpdir):
saved_hyperopt_results_legacy, tmpdir):
csv_file = Path(tmpdir) / "test.csv" csv_file = Path(tmpdir) / "test.csv"
for res in (saved_hyperopt_results, saved_hyperopt_results_legacy): mocker.patch(
mocker.patch( 'freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist',
'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', return_value=True
MagicMock(return_value=res)
) )
args = [ def fake_iterator(*args, **kwargs):
"hyperopt-list", yield from [saved_hyperopt_results]
"--no-details",
"--no-color", mocker.patch(
] 'freqtrade.optimize.hyperopt_tools.HyperoptTools._read_results',
pargs = get_args(args) side_effect=fake_iterator
pargs['config'] = None )
start_hyperopt_list(pargs)
captured = capsys.readouterr() args = [
assert all(x in captured.out "hyperopt-list",
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", "--no-details",
" 6/12", " 7/12", " 8/12", " 9/12", " 10/12", "--no-color",
" 11/12", " 12/12"]) ]
args = [ pargs = get_args(args)
"hyperopt-list", pargs['config'] = None
"--best", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
] for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12",
pargs = get_args(args) " 6/12", " 7/12", " 8/12", " 9/12", " 10/12",
pargs['config'] = None " 11/12", " 12/12"])
start_hyperopt_list(pargs) args = [
captured = capsys.readouterr() "hyperopt-list",
assert all(x in captured.out "--best",
for x in [" 1/12", " 5/12", " 10/12"]) "--no-details",
assert all(x not in captured.out "--no-color",
for x in [" 2/12", " 3/12", " 4/12", " 6/12", " 7/12", " 8/12", " 9/12", ]
" 11/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--profitable", captured = capsys.readouterr()
"--no-details", assert all(x in captured.out
"--no-color", for x in [" 1/12", " 5/12", " 10/12"])
] assert all(x not in captured.out
pargs = get_args(args) for x in [" 2/12", " 3/12", " 4/12", " 6/12", " 7/12", " 8/12", " 9/12",
pargs['config'] = None " 11/12", " 12/12"])
start_hyperopt_list(pargs) args = [
captured = capsys.readouterr() "hyperopt-list",
assert all(x in captured.out "--profitable",
for x in [" 2/12", " 10/12"]) "--no-details",
assert all(x not in captured.out "--no-color",
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", ]
" 11/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--profitable", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
] for x in [" 2/12", " 10/12"])
pargs = get_args(args) assert all(x not in captured.out
pargs['config'] = None for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
start_hyperopt_list(pargs) " 11/12", " 12/12"])
captured = capsys.readouterr() args = [
assert all(x in captured.out "hyperopt-list",
for x in [" 2/12", " 10/12", "Best result:", "Buy hyperspace params", "--profitable",
"Sell hyperspace params", "ROI table", "Stoploss"]) "--no-color",
assert all(x not in captured.out ]
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", pargs = get_args(args)
" 11/12", " 12/12"]) pargs['config'] = None
args = [ start_hyperopt_list(pargs)
"hyperopt-list", captured = capsys.readouterr()
"--no-details", assert all(x in captured.out
"--no-color", for x in [" 2/12", " 10/12", "Best result:", "Buy hyperspace params",
"--min-trades", "20", "Sell hyperspace params", "ROI table", "Stoploss"])
] assert all(x not in captured.out
pargs = get_args(args) for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
pargs['config'] = None " 11/12", " 12/12"])
start_hyperopt_list(pargs) args = [
captured = capsys.readouterr() "hyperopt-list",
assert all(x in captured.out "--no-details",
for x in [" 3/12", " 6/12", " 7/12", " 9/12", " 11/12"]) "--no-color",
assert all(x not in captured.out "--min-trades", "20",
for x in [" 1/12", " 2/12", " 4/12", " 5/12", " 8/12", " 10/12", " 12/12"]) ]
args = [ pargs = get_args(args)
"hyperopt-list", pargs['config'] = None
"--profitable", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
"--max-trades", "20", for x in [" 3/12", " 6/12", " 7/12", " 9/12", " 11/12"])
] assert all(x not in captured.out
pargs = get_args(args) for x in [" 1/12", " 2/12", " 4/12", " 5/12", " 8/12", " 10/12", " 12/12"])
pargs['config'] = None args = [
start_hyperopt_list(pargs) "hyperopt-list",
captured = capsys.readouterr() "--profitable",
assert all(x in captured.out "--no-details",
for x in [" 2/12", " 10/12"]) "--no-color",
assert all(x not in captured.out "--max-trades", "20",
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", ]
" 11/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--profitable", captured = capsys.readouterr()
"--no-details", assert all(x in captured.out
"--no-color", for x in [" 2/12", " 10/12"])
"--min-avg-profit", "0.11", assert all(x not in captured.out
] for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
pargs = get_args(args) " 11/12", " 12/12"])
pargs['config'] = None args = [
start_hyperopt_list(pargs) "hyperopt-list",
captured = capsys.readouterr() "--profitable",
assert all(x in captured.out "--no-details",
for x in [" 2/12"]) "--no-color",
assert all(x not in captured.out "--min-avg-profit", "0.11",
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", ]
" 10/12", " 11/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
"--max-avg-profit", "0.10", for x in [" 2/12"])
] assert all(x not in captured.out
pargs = get_args(args) for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
pargs['config'] = None " 10/12", " 11/12", " 12/12"])
start_hyperopt_list(pargs) args = [
captured = capsys.readouterr() "hyperopt-list",
assert all(x in captured.out "--no-details",
for x in [" 1/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", "--no-color",
" 11/12"]) "--max-avg-profit", "0.10",
assert all(x not in captured.out ]
for x in [" 2/12", " 4/12", " 10/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
"--min-total-profit", "0.4", for x in [" 1/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
] " 11/12"])
pargs = get_args(args) assert all(x not in captured.out
pargs['config'] = None for x in [" 2/12", " 4/12", " 10/12", " 12/12"])
start_hyperopt_list(pargs) args = [
captured = capsys.readouterr() "hyperopt-list",
assert all(x in captured.out "--no-details",
for x in [" 10/12"]) "--no-color",
assert all(x not in captured.out "--min-total-profit", "0.4",
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", ]
" 9/12", " 11/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
"--max-total-profit", "0.4", for x in [" 10/12"])
] assert all(x not in captured.out
pargs = get_args(args) for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12",
pargs['config'] = None " 9/12", " 11/12", " 12/12"])
start_hyperopt_list(pargs) args = [
captured = capsys.readouterr() "hyperopt-list",
assert all(x in captured.out "--no-details",
for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", "--no-color",
" 9/12", " 11/12"]) "--max-total-profit", "0.4",
assert all(x not in captured.out ]
for x in [" 4/12", " 10/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
"--min-objective", "0.1", for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12",
] " 9/12", " 11/12"])
pargs = get_args(args) assert all(x not in captured.out
pargs['config'] = None for x in [" 4/12", " 10/12", " 12/12"])
start_hyperopt_list(pargs) args = [
captured = capsys.readouterr() "hyperopt-list",
assert all(x in captured.out "--no-details",
for x in [" 10/12"]) "--no-color",
assert all(x not in captured.out "--min-objective", "0.1",
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", ]
" 9/12", " 11/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--max-objective", "0.1", assert all(x in captured.out
] for x in [" 10/12"])
pargs = get_args(args) assert all(x not in captured.out
pargs['config'] = None for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12",
start_hyperopt_list(pargs) " 9/12", " 11/12", " 12/12"])
captured = capsys.readouterr() args = [
assert all(x in captured.out "hyperopt-list",
for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", "--no-details",
" 9/12", " 11/12"]) "--max-objective", "0.1",
assert all(x not in captured.out ]
for x in [" 4/12", " 10/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--profitable", captured = capsys.readouterr()
"--no-details", assert all(x in captured.out
"--no-color", for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12",
"--min-avg-time", "2000", " 9/12", " 11/12"])
] assert all(x not in captured.out
pargs = get_args(args) for x in [" 4/12", " 10/12", " 12/12"])
pargs['config'] = None args = [
start_hyperopt_list(pargs) "hyperopt-list",
captured = capsys.readouterr() "--profitable",
assert all(x in captured.out "--no-details",
for x in [" 10/12"]) "--no-color",
assert all(x not in captured.out "--min-avg-time", "2000",
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", ]
" 8/12", " 9/12", " 11/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
"--max-avg-time", "1500", for x in [" 10/12"])
] assert all(x not in captured.out
pargs = get_args(args) for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12",
pargs['config'] = None " 8/12", " 9/12", " 11/12", " 12/12"])
start_hyperopt_list(pargs) args = [
captured = capsys.readouterr() "hyperopt-list",
assert all(x in captured.out "--no-details",
for x in [" 2/12", " 6/12"]) "--no-color",
assert all(x not in captured.out "--max-avg-time", "1500",
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 7/12", " 8/12" ]
" 9/12", " 10/12", " 11/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
"--export-csv", for x in [" 2/12", " 6/12"])
str(csv_file), assert all(x not in captured.out
] for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 7/12", " 8/12"
pargs = get_args(args) " 9/12", " 10/12", " 11/12", " 12/12"])
pargs['config'] = None args = [
start_hyperopt_list(pargs) "hyperopt-list",
captured = capsys.readouterr() "--no-details",
log_has("CSV file created: test_file.csv", caplog) "--no-color",
assert csv_file.is_file() "--export-csv",
line = csv_file.read_text() str(csv_file),
assert ('Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in line ]
or "Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,2 days 17:30:00,0.43662" in line) pargs = get_args(args)
csv_file.unlink() pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
log_has("CSV file created: test_file.csv", caplog)
assert csv_file.is_file()
line = csv_file.read_text()
assert ('Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in line
or "Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,2 days 17:30:00,0.43662" in line)
csv_file.unlink()
def test_hyperopt_show(mocker, capsys, saved_hyperopt_results): def test_hyperopt_show(mocker, capsys, saved_hyperopt_results):
mocker.patch( mocker.patch(
'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', 'freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist',
MagicMock(return_value=saved_hyperopt_results) return_value=True
)
def fake_iterator(*args, **kwargs):
yield from [saved_hyperopt_results]
mocker.patch(
'freqtrade.optimize.hyperopt_tools.HyperoptTools._read_results',
side_effect=fake_iterator
) )
mocker.patch('freqtrade.commands.hyperopt_commands.show_backtest_result') mocker.patch('freqtrade.commands.hyperopt_commands.show_backtest_result')

View File

@ -360,7 +360,7 @@ def get_default_conf(testdatadir):
"user_data_dir": Path("user_data"), "user_data_dir": Path("user_data"),
"verbosity": 3, "verbosity": 3,
"strategy_path": str(Path(__file__).parent / "strategy" / "strats"), "strategy_path": str(Path(__file__).parent / "strategy" / "strats"),
"strategy": "DefaultStrategy", "strategy": "StrategyTestV2",
"disableparamexport": True, "disableparamexport": True,
"internals": {}, "internals": {},
"export": "none", "export": "none",
@ -1851,138 +1851,6 @@ def open_trade():
) )
@pytest.fixture
def saved_hyperopt_results_legacy():
return [
{
'loss': 0.4366182531161519,
'params_dict': {
'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501
'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501
'results_metrics': {'trade_count': 2, 'avg_profit': -1.254995, 'median_profit': -1.2222, 'total_profit': -0.00125625, 'profit': -2.50999, 'duration': 3930.0}, # noqa: E501
'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501
'total_profit': -0.00125625,
'current_epoch': 1,
'is_initial_point': True,
'is_best': True
}, {
'loss': 20.0,
'params_dict': {
'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501
'params_details': {
'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # noqa: E501
'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501
'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501
'stoploss': {'stoploss': -0.338070047333259}},
'results_metrics': {'trade_count': 1, 'avg_profit': 0.12357, 'median_profit': -1.2222, 'total_profit': 6.185e-05, 'profit': 0.12357, 'duration': 1200.0}, # noqa: E501
'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501
'total_profit': 6.185e-05,
'current_epoch': 2,
'is_initial_point': True,
'is_best': False
}, {
'loss': 14.241196856510731,
'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, # noqa: E501
'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, # noqa: E501
'results_metrics': {'trade_count': 621, 'avg_profit': -0.43883302093397747, 'median_profit': -1.2222, 'total_profit': -0.13639474, 'profit': -272.515306, 'duration': 1691.207729468599}, # noqa: E501
'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 min.', # noqa: E501
'total_profit': -0.13639474,
'current_epoch': 3,
'is_initial_point': True,
'is_best': False
}, {
'loss': 100000,
'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, # noqa: E501
'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501
'results_metrics': {'trade_count': 0, 'avg_profit': None, 'median_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501
'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501
'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False
}, {
'loss': 0.22195522184191518,
'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501
'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, # noqa: E501
'results_metrics': {'trade_count': 14, 'avg_profit': -0.3539515, 'median_profit': -1.2222, 'total_profit': -0.002480140000000001, 'profit': -4.955321, 'duration': 3402.8571428571427}, # noqa: E501
'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 min.', # noqa: E501
'total_profit': -0.002480140000000001,
'current_epoch': 5,
'is_initial_point': True,
'is_best': True
}, {
'loss': 0.545315889154162,
'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, # noqa: E501
'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, # noqa: E501
'results_metrics': {'trade_count': 39, 'avg_profit': -0.21400679487179478, 'median_profit': -1.2222, 'total_profit': -0.0041773, 'profit': -8.346264999999997, 'duration': 636.9230769230769}, # noqa: E501
'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 min.', # noqa: E501
'total_profit': -0.0041773,
'current_epoch': 6,
'is_initial_point': True,
'is_best': False
}, {
'loss': 4.713497421432944,
'params_dict': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, # noqa: E501
'params_details': {
'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501
'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501
'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'median_profit': -1.2222, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, # noqa: E501
'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501
'total_profit': -0.06339929,
'current_epoch': 7,
'is_initial_point': True,
'is_best': False
}, {
'loss': 20.0, # noqa: E501
'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, # noqa: E501
'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, # noqa: E501
'results_metrics': {'trade_count': 1, 'avg_profit': 0.0, 'median_profit': 0.0, 'total_profit': 0.0, 'profit': 0.0, 'duration': 5340.0}, # noqa: E501
'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 min.', # noqa: E501
'total_profit': 0.0,
'current_epoch': 8,
'is_initial_point': True,
'is_best': False
}, {
'loss': 2.4731817780991223,
'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, # noqa: E501
'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, # noqa: E501
'results_metrics': {'trade_count': 229, 'avg_profit': -0.38433433624454144, 'median_profit': -1.2222, 'total_profit': -0.044050070000000004, 'profit': -88.01256299999999, 'duration': 6505.676855895196}, # noqa: E501
'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 min.', # noqa: E501
'total_profit': -0.044050070000000004, # noqa: E501
'current_epoch': 9,
'is_initial_point': True,
'is_best': False
}, {
'loss': -0.2604606005845212, # noqa: E501
'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, # noqa: E501
'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, # noqa: E501
'results_metrics': {'trade_count': 4, 'avg_profit': 0.1080385, 'median_profit': -1.2222, 'total_profit': 0.00021629, 'profit': 0.432154, 'duration': 2850.0}, # noqa: E501
'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 min.', # noqa: E501
'total_profit': 0.00021629,
'current_epoch': 10,
'is_initial_point': True,
'is_best': True
}, {
'loss': 4.876465945994304, # noqa: E501
'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, # noqa: E501
'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, # noqa: E501
'results_metrics': {'trade_count': 117, 'avg_profit': -1.2698609145299145, 'median_profit': -1.2222, 'total_profit': -0.07436117, 'profit': -148.573727, 'duration': 4282.5641025641025}, # noqa: E501
'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 min.', # noqa: E501
'total_profit': -0.07436117,
'current_epoch': 11,
'is_initial_point': True,
'is_best': False
}, {
'loss': 100000,
'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, # noqa: E501
'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, # noqa: E501
'results_metrics': {'trade_count': 0, 'avg_profit': None, 'median_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501
'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501
'total_profit': 0,
'current_epoch': 12,
'is_initial_point': True,
'is_best': False
}
]
@pytest.fixture @pytest.fixture
def saved_hyperopt_results(): def saved_hyperopt_results():
hyperopt_res = [ hyperopt_res = [

View File

@ -33,7 +33,7 @@ def mock_trade_1(fee):
open_rate=0.123, open_rate=0.123,
exchange='binance', exchange='binance',
open_order_id='dry_run_buy_12345', open_order_id='dry_run_buy_12345',
strategy='DefaultStrategy', strategy='StrategyTestV2',
timeframe=5, timeframe=5,
) )
o = Order.parse_from_ccxt_object(mock_order_1(), 'ETH/BTC', 'buy') o = Order.parse_from_ccxt_object(mock_order_1(), 'ETH/BTC', 'buy')
@ -87,7 +87,7 @@ def mock_trade_2(fee):
exchange='binance', exchange='binance',
is_open=False, is_open=False,
open_order_id='dry_run_sell_12345', open_order_id='dry_run_sell_12345',
strategy='DefaultStrategy', strategy='StrategyTestV2',
timeframe=5, timeframe=5,
sell_reason='sell_signal', sell_reason='sell_signal',
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
@ -146,7 +146,7 @@ def mock_trade_3(fee):
close_profit_abs=0.000155, close_profit_abs=0.000155,
exchange='binance', exchange='binance',
is_open=False, is_open=False,
strategy='DefaultStrategy', strategy='StrategyTestV2',
timeframe=5, timeframe=5,
sell_reason='roi', sell_reason='roi',
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
@ -189,7 +189,7 @@ def mock_trade_4(fee):
open_rate=0.123, open_rate=0.123,
exchange='binance', exchange='binance',
open_order_id='prod_buy_12345', open_order_id='prod_buy_12345',
strategy='DefaultStrategy', strategy='StrategyTestV2',
timeframe=5, timeframe=5,
) )
o = Order.parse_from_ccxt_object(mock_order_4(), 'ETC/BTC', 'buy') o = Order.parse_from_ccxt_object(mock_order_4(), 'ETC/BTC', 'buy')

View File

@ -93,7 +93,7 @@ def test_load_backtest_data_new_format(testdatadir):
def test_load_backtest_data_multi(testdatadir): def test_load_backtest_data_multi(testdatadir):
filename = testdatadir / "backtest-result_multistrat.json" filename = testdatadir / "backtest-result_multistrat.json"
for strategy in ('DefaultStrategy', 'TestStrategy'): for strategy in ('StrategyTestV2', 'TestStrategy'):
bt_data = load_backtest_data(filename, strategy=strategy) bt_data = load_backtest_data(filename, strategy=strategy)
assert isinstance(bt_data, DataFrame) assert isinstance(bt_data, DataFrame)
assert set(bt_data.columns) == set(BT_DATA_COLUMNS_MID) assert set(bt_data.columns) == set(BT_DATA_COLUMNS_MID)
@ -128,7 +128,7 @@ def test_load_trades_from_db(default_conf, fee, mocker):
for col in BT_DATA_COLUMNS: for col in BT_DATA_COLUMNS:
if col not in ['index', 'open_at_end']: if col not in ['index', 'open_at_end']:
assert col in trades.columns assert col in trades.columns
trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='DefaultStrategy') trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='StrategyTestV2')
assert len(trades) == 4 assert len(trades) == 4
trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='NoneStrategy') trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='NoneStrategy')
assert len(trades) == 0 assert len(trades) == 0
@ -186,7 +186,7 @@ def test_load_trades(default_conf, mocker):
db_url=default_conf.get('db_url'), db_url=default_conf.get('db_url'),
exportfilename=default_conf.get('exportfilename'), exportfilename=default_conf.get('exportfilename'),
no_trades=False, no_trades=False,
strategy="DefaultStrategy", strategy="StrategyTestV2",
) )
assert db_mock.call_count == 1 assert db_mock.call_count == 1

View File

@ -119,7 +119,7 @@ def test_ohlcv_fill_up_missing_data2(caplog):
# 3rd candle has been filled # 3rd candle has been filled
row = data2.loc[2, :] row = data2.loc[2, :]
assert row['volume'] == 0 assert row['volume'] == 0
# close shoult match close of previous candle # close should match close of previous candle
assert row['close'] == data.loc[1, 'close'] assert row['close'] == data.loc[1, 'close']
assert row['open'] == row['close'] assert row['open'] == row['close']
assert row['high'] == row['close'] assert row['high'] == row['close']

View File

@ -66,7 +66,7 @@ def test_historic_ohlcv_dataformat(mocker, default_conf, ohlcv_history):
hdf5loadmock.assert_not_called() hdf5loadmock.assert_not_called()
jsonloadmock.assert_called_once() jsonloadmock.assert_called_once()
# Swiching to dataformat hdf5 # Switching to dataformat hdf5
hdf5loadmock.reset_mock() hdf5loadmock.reset_mock()
jsonloadmock.reset_mock() jsonloadmock.reset_mock()
default_conf["dataformat_ohlcv"] = "hdf5" default_conf["dataformat_ohlcv"] = "hdf5"

View File

@ -133,8 +133,8 @@ def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog,
load_pair_history(datadir=tmpdir1, timeframe='1m', pair='MEME/BTC') load_pair_history(datadir=tmpdir1, timeframe='1m', pair='MEME/BTC')
assert file.is_file() assert file.is_file()
assert log_has_re( assert log_has_re(
'Download history data for pair: "MEME/BTC", timeframe: 1m ' r'Download history data for pair: "MEME/BTC" \(0/1\), timeframe: 1m '
'and store in .*', caplog r'and store in .*', caplog
) )
@ -200,15 +200,15 @@ def test_load_cached_data_for_updating(mocker, testdatadir) -> None:
assert start_ts == test_data[0][0] - 1000 assert start_ts == test_data[0][0] - 1000
# timeframe starts in the center of the cached data # timeframe starts in the center of the cached data
# should return the chached data w/o the last item # should return the cached data w/o the last item
timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0) timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0)
data, start_ts = _load_cached_data_for_updating('UNITTEST/BTC', '1m', timerange, data_handler) data, start_ts = _load_cached_data_for_updating('UNITTEST/BTC', '1m', timerange, data_handler)
assert_frame_equal(data, test_data_df.iloc[:-1]) assert_frame_equal(data, test_data_df.iloc[:-1])
assert test_data[-2][0] <= start_ts < test_data[-1][0] assert test_data[-2][0] <= start_ts < test_data[-1][0]
# timeframe starts after the chached data # timeframe starts after the cached data
# should return the chached data w/o the last item # should return the cached data w/o the last item
timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 100, 0) timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 100, 0)
data, start_ts = _load_cached_data_for_updating('UNITTEST/BTC', '1m', timerange, data_handler) data, start_ts = _load_cached_data_for_updating('UNITTEST/BTC', '1m', timerange, data_handler)
assert_frame_equal(data, test_data_df.iloc[:-1]) assert_frame_equal(data, test_data_df.iloc[:-1])
@ -278,8 +278,10 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None:
return_value=None) return_value=None)
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick) mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick)
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
_download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='1m') _download_pair_history(datadir=testdatadir, exchange=exchange, pair="UNITTEST/BTC",
_download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='3m') timeframe='1m')
_download_pair_history(datadir=testdatadir, exchange=exchange, pair="UNITTEST/BTC",
timeframe='3m')
assert json_dump_mock.call_count == 2 assert json_dump_mock.call_count == 2
@ -378,10 +380,10 @@ def test_file_dump_json_tofile(testdatadir) -> None:
def test_get_timerange(default_conf, mocker, testdatadir) -> None: def test_get_timerange(default_conf, mocker, testdatadir) -> None:
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
data = strategy.ohlcvdata_to_dataframe( data = strategy.advise_all_indicators(
load_data( load_data(
datadir=testdatadir, datadir=testdatadir,
timeframe='1m', timeframe='1m',
@ -396,10 +398,10 @@ def test_get_timerange(default_conf, mocker, testdatadir) -> None:
def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) -> None: def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) -> None:
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
data = strategy.ohlcvdata_to_dataframe( data = strategy.advise_all_indicators(
load_data( load_data(
datadir=testdatadir, datadir=testdatadir,
timeframe='1m', timeframe='1m',
@ -420,11 +422,11 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir)
def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> None: def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> None:
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
timerange = TimeRange('index', 'index', 200, 250) timerange = TimeRange('index', 'index', 200, 250)
data = strategy.ohlcvdata_to_dataframe( data = strategy.advise_all_indicators(
load_data( load_data(
datadir=testdatadir, datadir=testdatadir,
timeframe='5m', timeframe='5m',

View File

@ -42,6 +42,11 @@ EXCHANGES = {
'hasQuoteVolume': True, 'hasQuoteVolume': True,
'timeframe': '5m', 'timeframe': '5m',
}, },
'gateio': {
'pair': 'BTC/USDT',
'hasQuoteVolume': True,
'timeframe': '5m',
},
} }
@ -142,8 +147,8 @@ class TestCCXTExchange():
def test_ccxt_get_fee(self, exchange): def test_ccxt_get_fee(self, exchange):
exchange, exchangename = exchange exchange, exchangename = exchange
pair = EXCHANGES[exchangename]['pair'] pair = EXCHANGES[exchangename]['pair']
threshold = 0.01
assert 0 < exchange.get_fee(pair, 'limit', 'buy') < 1 assert 0 < exchange.get_fee(pair, 'limit', 'buy') < threshold
assert 0 < exchange.get_fee(pair, 'limit', 'sell') < 1 assert 0 < exchange.get_fee(pair, 'limit', 'sell') < threshold
assert 0 < exchange.get_fee(pair, 'market', 'buy') < 1 assert 0 < exchange.get_fee(pair, 'market', 'buy') < threshold
assert 0 < exchange.get_fee(pair, 'market', 'sell') < 1 assert 0 < exchange.get_fee(pair, 'market', 'sell') < threshold

View File

@ -591,7 +591,7 @@ def test_reload_markets_exception(default_conf, mocker, caplog):
@pytest.mark.parametrize("stake_currency", ['ETH', 'BTC', 'USDT']) @pytest.mark.parametrize("stake_currency", ['ETH', 'BTC', 'USDT'])
def test_validate_stake_currency(default_conf, stake_currency, mocker, caplog): def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
default_conf['stake_currency'] = stake_currency default_conf['stake_currency'] = stake_currency
api_mock = MagicMock() api_mock = MagicMock()
type(api_mock).load_markets = MagicMock(return_value={ type(api_mock).load_markets = MagicMock(return_value={
@ -605,7 +605,7 @@ def test_validate_stake_currency(default_conf, stake_currency, mocker, caplog):
Exchange(default_conf) Exchange(default_conf)
def test_validate_stake_currency_error(default_conf, mocker, caplog): def test_validate_stakecurrency_error(default_conf, mocker, caplog):
default_conf['stake_currency'] = 'XRP' default_conf['stake_currency'] = 'XRP'
api_mock = MagicMock() api_mock = MagicMock()
type(api_mock).load_markets = MagicMock(return_value={ type(api_mock).load_markets = MagicMock(return_value={
@ -621,6 +621,13 @@ def test_validate_stake_currency_error(default_conf, mocker, caplog):
'Available currencies are: BTC, ETH, USDT'): 'Available currencies are: BTC, ETH, USDT'):
Exchange(default_conf) Exchange(default_conf)
type(api_mock).load_markets = MagicMock(side_effect=ccxt.NetworkError('No connection.'))
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
with pytest.raises(OperationalException,
match=r'Could not load markets, therefore cannot start\. Please.*'):
Exchange(default_conf)
def test_get_quote_currencies(default_conf, mocker): def test_get_quote_currencies(default_conf, mocker):
ex = get_patched_exchange(mocker, default_conf) ex = get_patched_exchange(mocker, default_conf)
@ -1018,16 +1025,21 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice,
assert order['fee'] assert order['fee']
@pytest.mark.parametrize("side,amount,endprice", [ @pytest.mark.parametrize("side,rate,amount,endprice", [
("buy", 1, 25.566), # spread is 25.263-25.266
("buy", 100, 25.5672), # Requires interpolation ("buy", 25.564, 1, 25.566),
("buy", 1000, 25.575), # More than orderbook return ("buy", 25.564, 100, 25.5672), # Requires interpolation
("sell", 1, 25.563), ("buy", 25.590, 100, 25.5672), # Price above spread ... average is lower
("sell", 100, 25.5625), # Requires interpolation ("buy", 25.564, 1000, 25.575), # More than orderbook return
("sell", 1000, 25.5555), # More than orderbook return ("buy", 24.000, 100000, 25.200), # Run into max_slippage of 5%
("sell", 25.564, 1, 25.563),
("sell", 25.564, 100, 25.5625), # Requires interpolation
("sell", 25.510, 100, 25.5625), # price below spread - average is higher
("sell", 25.564, 1000, 25.5555), # More than orderbook return
("sell", 27, 10000, 25.65), # max-slippage 5%
]) ])
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_create_dry_run_order_market_fill(default_conf, mocker, side, amount, endprice, def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amount, endprice,
exchange_name, order_book_l2_usd): exchange_name, order_book_l2_usd):
default_conf['dry_run'] = True default_conf['dry_run'] = True
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
@ -1037,7 +1049,7 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, amount, en
) )
order = exchange.create_dry_run_order( order = exchange.create_dry_run_order(
pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=25.5) pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=rate)
assert 'id' in order assert 'id' in order
assert f'dry_run_{side}_' in order["id"] assert f'dry_run_{side}_' in order["id"]
assert order["side"] == side assert order["side"] == side
@ -1593,13 +1605,16 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
pairs = [('IOTA/ETH', '5m'), ('XRP/ETH', '5m')] pairs = [('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]
# empty dicts # empty dicts
assert not exchange._klines assert not exchange._klines
exchange.refresh_latest_ohlcv(pairs, cache=False) res = exchange.refresh_latest_ohlcv(pairs, cache=False)
# No caching # No caching
assert not exchange._klines assert not exchange._klines
assert len(res) == len(pairs)
assert exchange._api_async.fetch_ohlcv.call_count == 2 assert exchange._api_async.fetch_ohlcv.call_count == 2
exchange._api_async.fetch_ohlcv.reset_mock() exchange._api_async.fetch_ohlcv.reset_mock()
exchange.refresh_latest_ohlcv(pairs) res = exchange.refresh_latest_ohlcv(pairs)
assert len(res) == len(pairs)
assert log_has(f'Refreshing candle (OHLCV) data for {len(pairs)} pairs', caplog) assert log_has(f'Refreshing candle (OHLCV) data for {len(pairs)} pairs', caplog)
assert exchange._klines assert exchange._klines
@ -1616,12 +1631,16 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
assert exchange.klines(pair, copy=False) is exchange.klines(pair, copy=False) assert exchange.klines(pair, copy=False) is exchange.klines(pair, copy=False)
# test caching # test caching
exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]) res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')])
assert len(res) == len(pairs)
assert exchange._api_async.fetch_ohlcv.call_count == 2 assert exchange._api_async.fetch_ohlcv.call_count == 2
assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, " assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, "
f"timeframe {pairs[0][1]} ...", f"timeframe {pairs[0][1]} ...",
caplog) caplog)
res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m'), ('XRP/ETH', '1d')],
cache=False)
assert len(res) == 3
@pytest.mark.asyncio @pytest.mark.asyncio
@ -1873,6 +1892,31 @@ def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask,
assert log_has("Using cached sell rate for ETH/BTC.", caplog) assert log_has("Using cached sell rate for ETH/BTC.", caplog)
@pytest.mark.parametrize("entry,side,ask,bid,last,last_ab,expected", [
('buy', 'ask', None, 4, 4, 0, 4), # ask not available
('buy', 'ask', None, None, 4, 0, 4), # ask not available
('buy', 'bid', 6, None, 4, 0, 5), # bid not available
('buy', 'bid', None, None, 4, 0, 5), # No rate available
('sell', 'ask', None, 4, 4, 0, 4), # ask not available
('sell', 'ask', None, None, 4, 0, 4), # ask not available
('sell', 'bid', 6, None, 4, 0, 5), # bid not available
('sell', 'bid', None, None, 4, 0, 5), # bid not available
])
def test_get_ticker_rate_error(mocker, entry, default_conf, caplog, side, ask, bid,
last, last_ab, expected) -> None:
caplog.set_level(logging.DEBUG)
default_conf['bid_strategy']['ask_last_balance'] = last_ab
default_conf['bid_strategy']['price_side'] = side
default_conf['ask_strategy']['price_side'] = side
default_conf['ask_strategy']['ask_last_balance'] = last_ab
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
return_value={'ask': ask, 'last': last, 'bid': bid})
with pytest.raises(PricingError):
exchange.get_rate('ETH/BTC', refresh=True, side=entry)
@pytest.mark.parametrize('side,expected', [ @pytest.mark.parametrize('side,expected', [
('bid', 0.043936), # Value from order_book_l2 fiture - bids side ('bid', 0.043936), # Value from order_book_l2 fiture - bids side
('ask', 0.043949), # Value from order_book_l2 fiture - asks side ('ask', 0.043949), # Value from order_book_l2 fiture - asks side

View File

@ -52,4 +52,6 @@ def _build_backtest_dataframe(data):
# Ensure floats are in place # Ensure floats are in place
for column in ['open', 'high', 'low', 'close', 'volume']: for column in ['open', 'high', 'low', 'close', 'volume']:
frame[column] = frame[column].astype('float64') frame[column] = frame[column].astype('float64')
if 'buy_tag' not in columns:
frame['buy_tag'] = None
return frame return frame

View File

@ -16,7 +16,7 @@ def hyperopt_conf(default_conf):
hyperconf.update({ hyperconf.update({
'datadir': Path(default_conf['datadir']), 'datadir': Path(default_conf['datadir']),
'runmode': RunMode.HYPEROPT, 'runmode': RunMode.HYPEROPT,
'hyperopt': 'DefaultHyperOpt', 'hyperopt': 'HyperoptTestSepFile',
'hyperopt_loss': 'ShortTradeDurHyperOptLoss', 'hyperopt_loss': 'ShortTradeDurHyperOptLoss',
'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'),
'epochs': 1, 'epochs': 1,

View File

@ -11,7 +11,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_interface import IHyperOpt
class DefaultHyperOpt(IHyperOpt): class HyperoptTestSepFile(IHyperOpt):
""" """
Default hyperopt provided by the Freqtrade bot. Default hyperopt provided by the Freqtrade bot.
You can override it with your own Hyperopt You can override it with your own Hyperopt

View File

@ -1,6 +1,7 @@
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument
import random import random
from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
@ -85,7 +86,7 @@ def simple_backtest(config, contour, mocker, testdatadir) -> None:
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
data = load_data_test(contour, testdatadir) data = load_data_test(contour, testdatadir)
processed = backtesting.strategy.ohlcvdata_to_dataframe(data) processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
assert isinstance(processed, dict) assert isinstance(processed, dict)
results = backtesting.backtest( results = backtesting.backtest(
@ -107,7 +108,7 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(conf) backtesting = Backtesting(conf)
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
processed = backtesting.strategy.ohlcvdata_to_dataframe(data) processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
return { return {
'processed': processed, 'processed': processed,
@ -154,7 +155,7 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca
args = [ args = [
'backtesting', 'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'StrategyTestV2',
'--export', 'none' '--export', 'none'
] ]
@ -189,7 +190,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
args = [ args = [
'backtesting', 'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'StrategyTestV2',
'--datadir', '/foo/bar', '--datadir', '/foo/bar',
'--timeframe', '1m', '--timeframe', '1m',
'--enable-position-stacking', '--enable-position-stacking',
@ -239,7 +240,7 @@ def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog)
args = [ args = [
'backtesting', 'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'StrategyTestV2',
'--stake-amount', '1', '--stake-amount', '1',
'--starting-balance', '2' '--starting-balance', '2'
] ]
@ -250,7 +251,7 @@ def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog)
args = [ args = [
'backtesting', 'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'StrategyTestV2',
'--stake-amount', '1', '--stake-amount', '1',
'--starting-balance', '0.5' '--starting-balance', '0.5'
] ]
@ -268,7 +269,7 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
args = [ args = [
'backtesting', 'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'StrategyTestV2',
] ]
pargs = get_args(args) pargs = get_args(args)
start_backtesting(pargs) start_backtesting(pargs)
@ -289,7 +290,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
assert backtesting.config == default_conf assert backtesting.config == default_conf
assert backtesting.timeframe == '5m' assert backtesting.timeframe == '5m'
assert callable(backtesting.strategy.ohlcvdata_to_dataframe) assert callable(backtesting.strategy.advise_all_indicators)
assert callable(backtesting.strategy.advise_buy) assert callable(backtesting.strategy.advise_buy)
assert callable(backtesting.strategy.advise_sell) assert callable(backtesting.strategy.advise_sell)
assert isinstance(backtesting.strategy.dp, DataProvider) assert isinstance(backtesting.strategy.dp, DataProvider)
@ -301,7 +302,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None: def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None:
patch_exchange(mocker) patch_exchange(mocker)
del default_conf['timeframe'] del default_conf['timeframe']
default_conf['strategy_list'] = ['DefaultStrategy', default_conf['strategy_list'] = ['StrategyTestV2',
'SampleStrategy'] 'SampleStrategy']
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
@ -335,14 +336,14 @@ def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
fill_up_missing=True) fill_up_missing=True)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
processed = backtesting.strategy.ohlcvdata_to_dataframe(data) processed = backtesting.strategy.advise_all_indicators(data)
assert len(processed['UNITTEST/BTC']) == 102 assert len(processed['UNITTEST/BTC']) == 102
# Load strategy to compare the result between Backtesting function and strategy are the same # Load strategy to compare the result between Backtesting function and strategy are the same
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
processed2 = strategy.ohlcvdata_to_dataframe(data) processed2 = strategy.advise_all_indicators(data)
assert processed['UNITTEST/BTC'].equals(processed2['UNITTEST/BTC']) assert processed['UNITTEST/BTC'].equals(processed2['UNITTEST/BTC'])
@ -440,6 +441,15 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) ->
with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting.'): with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting.'):
Backtesting(default_conf) Backtesting(default_conf)
default_conf.update({
'pairlists': [{"method": "StaticPairList"}],
'timeframe_detail': '1d',
})
with pytest.raises(OperationalException,
match='Detail timeframe must be smaller than strategy timeframe.'):
Backtesting(default_conf)
def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, tickers) -> None: def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, tickers) -> None:
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
@ -472,7 +482,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti
Backtesting(default_conf) Backtesting(default_conf)
# Multiple strategies # Multiple strategies
default_conf['strategy_list'] = ['DefaultStrategy', 'TestStrategyLegacy'] default_conf['strategy_list'] = ['StrategyTestV2', 'TestStrategyLegacyV1']
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match='PrecisionFilter not allowed for backtesting multiple strategies.'): match='PrecisionFilter not allowed for backtesting multiple strategies.'):
Backtesting(default_conf) Backtesting(default_conf)
@ -490,7 +500,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
pair = 'UNITTEST/BTC' pair = 'UNITTEST/BTC'
row = [ row = [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0),
1, # Sell 1, # Buy
0.001, # Open 0.001, # Open
0.0011, # Close 0.0011, # Close
0, # Sell 0, # Sell
@ -535,6 +545,90 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
trade = backtesting._enter_trade(pair, row=row) trade = backtesting._enter_trade(pair, row=row)
assert trade is None assert trade is None
backtesting.cleanup()
def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
default_conf['use_sell_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
patch_exchange(mocker)
default_conf['timeframe_detail'] = '1m'
default_conf['max_open_trades'] = 2
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
pair = 'UNITTEST/BTC'
row = [
pd.Timestamp(year=2020, month=1, day=1, hour=4, minute=55, tzinfo=timezone.utc),
1, # Buy
200, # Open
201, # Close
0, # Sell
195, # Low
201.5, # High
'', # Buy Signal Name
]
trade = backtesting._enter_trade(pair, row=row)
assert isinstance(trade, LocalTrade)
row_sell = [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0, tzinfo=timezone.utc),
0, # Buy
200, # Open
201, # Close
0, # Sell
195, # Low
210.5, # High
'', # Buy Signal Name
]
row_detail = pd.DataFrame(
[
[
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0, tzinfo=timezone.utc),
1, 200, 199, 0, 197, 200.1, '',
], [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=1, tzinfo=timezone.utc),
0, 199, 199.5, 0, 199, 199.7, '',
], [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=2, tzinfo=timezone.utc),
0, 199.5, 200.5, 0, 199, 200.8, '',
], [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=3, tzinfo=timezone.utc),
0, 200.5, 210.5, 0, 193, 210.5, '', # ROI sell (?)
], [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=4, tzinfo=timezone.utc),
0, 200, 199, 0, 193, 200.1, '',
],
], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag"]
)
# No data available.
res = backtesting._get_sell_trade_entry(trade, row_sell)
assert res is not None
assert res.sell_reason == SellType.ROI.value
assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc)
# Enter new trade
trade = backtesting._enter_trade(pair, row=row)
assert isinstance(trade, LocalTrade)
# Assign empty ... no result.
backtesting.detail_data[pair] = pd.DataFrame(
[], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag"])
res = backtesting._get_sell_trade_entry(trade, row)
assert res is None
# Assign backtest-detail data
backtesting.detail_data[pair] = row_detail
res = backtesting._get_sell_trade_entry(trade, row_sell)
assert res is not None
assert res.sell_reason == SellType.ROI.value
# Sell at minute 3 (not available above!)
assert res.close_date_utc == datetime(2020, 1, 1, 5, 3, tzinfo=timezone.utc)
assert round(res.close_rate, 3) == round(209.0225, 3)
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
default_conf['use_sell_signal'] = False default_conf['use_sell_signal'] = False
@ -547,7 +641,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
timerange = TimeRange('date', None, 1517227800, 0) timerange = TimeRange('date', None, 1517227800, 0)
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
timerange=timerange) timerange=timerange)
processed = backtesting.strategy.ohlcvdata_to_dataframe(data) processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
result = backtesting.backtest( result = backtesting.backtest(
processed=processed, processed=processed,
@ -581,7 +675,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
'initial_stop_loss_ratio': [-0.1, -0.1], 'initial_stop_loss_ratio': [-0.1, -0.1],
'stop_loss_abs': [0.0940005, 0.09272236], 'stop_loss_abs': [0.0940005, 0.09272236],
'stop_loss_ratio': [-0.1, -0.1], 'stop_loss_ratio': [-0.1, -0.1],
'min_rate': [0.1038, 0.10302485], 'min_rate': [0.10370188, 0.10300000000000001],
'max_rate': [0.10501, 0.1038888], 'max_rate': [0.10501, 0.1038888],
'is_open': [False, False], 'is_open': [False, False],
'buy_tag': [None, None], 'buy_tag': [None, None],
@ -612,7 +706,7 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
timerange = TimeRange.parse_timerange('1510688220-1510700340') timerange = TimeRange.parse_timerange('1510688220-1510700340')
data = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'], data = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'],
timerange=timerange) timerange=timerange)
processed = backtesting.strategy.ohlcvdata_to_dataframe(data) processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
results = backtesting.backtest( results = backtesting.backtest(
processed=processed, processed=processed,
@ -631,7 +725,7 @@ def test_processed(default_conf, mocker, testdatadir) -> None:
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
dict_of_tickerrows = load_data_test('raise', testdatadir) dict_of_tickerrows = load_data_test('raise', testdatadir)
dataframes = backtesting.strategy.ohlcvdata_to_dataframe(dict_of_tickerrows) dataframes = backtesting.strategy.advise_all_indicators(dict_of_tickerrows)
dataframe = dataframes['UNITTEST/BTC'] dataframe = dataframes['UNITTEST/BTC']
cols = dataframe.columns cols = dataframe.columns
# assert the dataframe got some of the indicator columns # assert the dataframe got some of the indicator columns
@ -691,7 +785,7 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir,
def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):
# Override the default buy trend function in our default_strategy # Override the default buy trend function in our StrategyTestV2
def fun(dataframe=None, pair=None): def fun(dataframe=None, pair=None):
buy_value = 1 buy_value = 1
sell_value = 1 sell_value = 1
@ -707,7 +801,7 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):
def test_backtest_only_sell(mocker, default_conf, testdatadir): def test_backtest_only_sell(mocker, default_conf, testdatadir):
# Override the default buy trend function in our default_strategy # Override the default buy trend function in our StrategyTestV2
def fun(dataframe=None, pair=None): def fun(dataframe=None, pair=None):
buy_value = 0 buy_value = 0
sell_value = 1 sell_value = 1
@ -739,8 +833,13 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
# 100 buys signals # 100 buys signals
results = result['results'] results = result['results']
assert len(results) == 100 assert len(results) == 100
# Cached data should be 200 (no change since required_startup is 0) # Cached data should be 200
assert len(backtesting.dataprovider.get_analyzed_dataframe('UNITTEST/BTC', '1m')[0]) == 200 analyzed_df = backtesting.dataprovider.get_analyzed_dataframe('UNITTEST/BTC', '1m')[0]
assert len(analyzed_df) == 200
# Expect last candle to be 1 below end date (as the last candle is assumed as "incomplete"
# during backtesting)
expected_last_candle_date = backtest_conf['end_date'] - timedelta(minutes=1)
assert analyzed_df.iloc[-1]['date'].to_pydatetime() == expected_last_candle_date
# One trade was force-closed at the end # One trade was force-closed at the end
assert len(results.loc[results['is_open']]) == 0 assert len(results.loc[results['is_open']]) == 0
@ -772,7 +871,8 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
data = trim_dictlist(data, -500) data = trim_dictlist(data, -500)
# Remove data for one pair from the beginning of the data # Remove data for one pair from the beginning of the data
data[pair] = data[pair][tres:].reset_index() if tres > 0:
data[pair] = data[pair][tres:].reset_index()
default_conf['timeframe'] = '5m' default_conf['timeframe'] = '5m'
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
@ -780,7 +880,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
backtesting.strategy.advise_buy = _trend_alternate_hold # Override backtesting.strategy.advise_buy = _trend_alternate_hold # Override
backtesting.strategy.advise_sell = _trend_alternate_hold # Override backtesting.strategy.advise_sell = _trend_alternate_hold # Override
processed = backtesting.strategy.ohlcvdata_to_dataframe(data) processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
backtest_conf = { backtest_conf = {
'processed': processed, 'processed': processed,
@ -798,8 +898,11 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
assert len(evaluate_result_multi(results['results'], '5m', 3)) == 0 assert len(evaluate_result_multi(results['results'], '5m', 3)) == 0
# Cached data correctly removed amounts # Cached data correctly removed amounts
removed_candles = len(data[pair]) - 1 - backtesting.strategy.startup_candle_count offset = 1 if tres == 0 else 0
removed_candles = len(data[pair]) - offset - backtesting.strategy.startup_candle_count
assert len(backtesting.dataprovider.get_analyzed_dataframe(pair, '5m')[0]) == removed_candles assert len(backtesting.dataprovider.get_analyzed_dataframe(pair, '5m')[0]) == removed_candles
assert len(backtesting.dataprovider.get_analyzed_dataframe(
'NXT/BTC', '5m')[0]) == len(data['NXT/BTC']) - 1 - backtesting.strategy.startup_candle_count
backtest_conf = { backtest_conf = {
'processed': processed, 'processed': processed,
@ -825,7 +928,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
args = [ args = [
'backtesting', 'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'StrategyTestV2',
'--datadir', str(testdatadir), '--datadir', str(testdatadir),
'--timeframe', '1m', '--timeframe', '1m',
'--timerange', '1510694220-1510700340', '--timerange', '1510694220-1510700340',
@ -896,8 +999,8 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
'--enable-position-stacking', '--enable-position-stacking',
'--disable-max-market-positions', '--disable-max-market-positions',
'--strategy-list', '--strategy-list',
'DefaultStrategy', 'StrategyTestV2',
'TestStrategyLegacy', 'TestStrategyLegacyV1',
] ]
args = get_args(args) args = get_args(args)
start_backtesting(args) start_backtesting(args)
@ -919,8 +1022,8 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
'Backtesting with data from 2017-11-14 21:17:00 ' 'Backtesting with data from 2017-11-14 21:17:00 '
'up to 2017-11-14 22:58:00 (0 days).', 'up to 2017-11-14 22:58:00 (0 days).',
'Parameter --enable-position-stacking detected ...', 'Parameter --enable-position-stacking detected ...',
'Running backtesting for Strategy DefaultStrategy', 'Running backtesting for Strategy StrategyTestV2',
'Running backtesting for Strategy TestStrategyLegacy', 'Running backtesting for Strategy TestStrategyLegacyV1',
] ]
for line in exists: for line in exists:
@ -1000,8 +1103,8 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'--enable-position-stacking', '--enable-position-stacking',
'--disable-max-market-positions', '--disable-max-market-positions',
'--strategy-list', '--strategy-list',
'DefaultStrategy', 'StrategyTestV2',
'TestStrategyLegacy', 'TestStrategyLegacyV1',
] ]
args = get_args(args) args = get_args(args)
start_backtesting(args) start_backtesting(args)
@ -1017,8 +1120,8 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'Backtesting with data from 2017-11-14 21:17:00 ' 'Backtesting with data from 2017-11-14 21:17:00 '
'up to 2017-11-14 22:58:00 (0 days).', 'up to 2017-11-14 22:58:00 (0 days).',
'Parameter --enable-position-stacking detected ...', 'Parameter --enable-position-stacking detected ...',
'Running backtesting for Strategy DefaultStrategy', 'Running backtesting for Strategy StrategyTestV2',
'Running backtesting for Strategy TestStrategyLegacy', 'Running backtesting for Strategy TestStrategyLegacyV1',
] ]
for line in exists: for line in exists:
@ -1030,3 +1133,102 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
assert 'LEFT OPEN TRADES REPORT' in captured.out assert 'LEFT OPEN TRADES REPORT' in captured.out
assert '2017-11-14 21:17:00 -> 2017-11-14 22:58:00 | Max open trades : 1' in captured.out assert '2017-11-14 21:17:00 -> 2017-11-14 22:58:00 | Max open trades : 1' in captured.out
assert 'STRATEGY SUMMARY' in captured.out assert 'STRATEGY SUMMARY' in captured.out
@pytest.mark.filterwarnings("ignore:deprecated")
def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
caplog, testdatadir, capsys):
# Tests detail-data loading
default_conf.update({
"use_sell_signal": True,
"sell_profit_only": False,
"sell_profit_offset": 0.0,
"ignore_roi_if_buy_signal": False,
})
patch_exchange(mocker)
result1 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'],
'profit_ratio': [0.0, 0.0],
'profit_abs': [0.0, 0.0],
'open_date': pd.to_datetime(['2018-01-29 18:40:00',
'2018-01-30 03:30:00', ], utc=True
),
'close_date': pd.to_datetime(['2018-01-29 20:45:00',
'2018-01-30 05:35:00', ], utc=True),
'trade_duration': [235, 40],
'is_open': [False, False],
'stake_amount': [0.01, 0.01],
'open_rate': [0.104445, 0.10302485],
'close_rate': [0.104969, 0.103541],
'sell_reason': [SellType.ROI, SellType.ROI]
})
result2 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'],
'profit_ratio': [0.03, 0.01, 0.1],
'profit_abs': [0.01, 0.02, 0.2],
'open_date': pd.to_datetime(['2018-01-29 18:40:00',
'2018-01-30 03:30:00',
'2018-01-30 05:30:00'], utc=True
),
'close_date': pd.to_datetime(['2018-01-29 20:45:00',
'2018-01-30 05:35:00',
'2018-01-30 08:30:00'], utc=True),
'trade_duration': [47, 40, 20],
'is_open': [False, False, False],
'stake_amount': [0.01, 0.01, 0.01],
'open_rate': [0.104445, 0.10302485, 0.122541],
'close_rate': [0.104969, 0.103541, 0.123541],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
})
backtestmock = MagicMock(side_effect=[
{
'results': result1,
'config': default_conf,
'locks': [],
'rejected_signals': 20,
'final_balance': 1000,
},
{
'results': result2,
'config': default_conf,
'locks': [],
'rejected_signals': 20,
'final_balance': 1000,
}
])
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['XRP/ETH']))
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
patched_configuration_load_config_file(mocker, default_conf)
args = [
'backtesting',
'--config', 'config.json',
'--datadir', str(testdatadir),
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
'--timeframe', '5m',
'--timeframe-detail', '1m',
'--strategy-list',
'StrategyTestV2'
]
args = get_args(args)
start_backtesting(args)
# check the logs, that will contain the backtest result
exists = [
'Parameter -i/--timeframe detected ... Using timeframe: 5m ...',
'Parameter --timeframe-detail detected, using 1m for intra-candle backtesting ...',
f'Using data directory: {testdatadir} ...',
'Loading data from 2019-10-11 00:00:00 '
'up to 2019-10-13 11:10:00 (2 days).',
'Backtesting with data from 2019-10-11 01:40:00 '
'up to 2019-10-13 11:10:00 (2 days).',
'Running backtesting for Strategy StrategyTestV2',
]
for line in exists:
assert log_has(line, caplog)
captured = capsys.readouterr()
assert 'BACKTESTING REPORT' in captured.out
assert 'SELL REASON STATS' in captured.out
assert 'LEFT OPEN TRADES REPORT' in captured.out

View File

@ -16,7 +16,7 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca
args = [ args = [
'edge', 'edge',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'StrategyTestV2',
] ]
config = setup_optimize_configuration(get_args(args), RunMode.EDGE) config = setup_optimize_configuration(get_args(args), RunMode.EDGE)
@ -46,7 +46,7 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N
args = [ args = [
'edge', 'edge',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'StrategyTestV2',
'--datadir', '/foo/bar', '--datadir', '/foo/bar',
'--timeframe', '1m', '--timeframe', '1m',
'--timerange', ':100', '--timerange', ':100',
@ -80,7 +80,7 @@ def test_start(mocker, fee, edge_conf, caplog) -> None:
args = [ args = [
'edge', 'edge',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'StrategyTestV2',
] ]
pargs = get_args(args) pargs = get_args(args)
start_edge(pargs) start_edge(pargs)

View File

@ -22,7 +22,7 @@ from freqtrade.strategy.hyper import IntParameter
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
patched_configuration_load_config_file) patched_configuration_load_config_file)
from .hyperopts.default_hyperopt import DefaultHyperOpt from .hyperopts.hyperopt_test_sep_file import HyperoptTestSepFile
def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None: def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None:
@ -31,7 +31,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca
args = [ args = [
'hyperopt', 'hyperopt',
'--config', 'config.json', '--config', 'config.json',
'--hyperopt', 'DefaultHyperOpt', '--hyperopt', 'HyperoptTestSepFile',
] ]
config = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) config = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
@ -63,7 +63,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo
args = [ args = [
'hyperopt', 'hyperopt',
'--config', 'config.json', '--config', 'config.json',
'--hyperopt', 'DefaultHyperOpt', '--hyperopt', 'HyperoptTestSepFile',
'--datadir', '/foo/bar', '--datadir', '/foo/bar',
'--timeframe', '1m', '--timeframe', '1m',
'--timerange', ':100', '--timerange', ':100',
@ -115,7 +115,7 @@ def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None
args = [ args = [
'hyperopt', 'hyperopt',
'--config', 'config.json', '--config', 'config.json',
'--hyperopt', 'DefaultHyperOpt', '--hyperopt', 'HyperoptTestSepFile',
'--stake-amount', '1', '--stake-amount', '1',
'--starting-balance', '2' '--starting-balance', '2'
] ]
@ -125,7 +125,7 @@ def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None
args = [ args = [
'hyperopt', 'hyperopt',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'StrategyTestV2',
'--stake-amount', '1', '--stake-amount', '1',
'--starting-balance', '0.5' '--starting-balance', '0.5'
] ]
@ -136,7 +136,7 @@ def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None
def test_hyperoptresolver(mocker, default_conf, caplog) -> None: def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
hyperopt = DefaultHyperOpt hyperopt = HyperoptTestSepFile
delattr(hyperopt, 'populate_indicators') delattr(hyperopt, 'populate_indicators')
delattr(hyperopt, 'populate_buy_trend') delattr(hyperopt, 'populate_buy_trend')
delattr(hyperopt, 'populate_sell_trend') delattr(hyperopt, 'populate_sell_trend')
@ -144,7 +144,7 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver.load_object', 'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver.load_object',
MagicMock(return_value=hyperopt(default_conf)) MagicMock(return_value=hyperopt(default_conf))
) )
default_conf.update({'hyperopt': 'DefaultHyperOpt'}) default_conf.update({'hyperopt': 'HyperoptTestSepFile'})
x = HyperOptResolver.load_hyperopt(default_conf) x = HyperOptResolver.load_hyperopt(default_conf)
assert not hasattr(x, 'populate_indicators') assert not hasattr(x, 'populate_indicators')
assert not hasattr(x, 'populate_buy_trend') assert not hasattr(x, 'populate_buy_trend')
@ -184,7 +184,7 @@ def test_start_not_installed(mocker, default_conf, import_fails) -> None:
args = [ args = [
'hyperopt', 'hyperopt',
'--config', 'config.json', '--config', 'config.json',
'--hyperopt', 'DefaultHyperOpt', '--hyperopt', 'HyperoptTestSepFile',
'--hyperopt-path', '--hyperopt-path',
str(Path(__file__).parent / "hyperopts"), str(Path(__file__).parent / "hyperopts"),
'--epochs', '5', '--epochs', '5',
@ -205,7 +205,7 @@ def test_start(mocker, hyperopt_conf, caplog) -> None:
args = [ args = [
'hyperopt', 'hyperopt',
'--config', 'config.json', '--config', 'config.json',
'--hyperopt', 'DefaultHyperOpt', '--hyperopt', 'HyperoptTestSepFile',
'--hyperopt-loss', 'SharpeHyperOptLossDaily', '--hyperopt-loss', 'SharpeHyperOptLossDaily',
'--epochs', '5' '--epochs', '5'
] ]
@ -229,7 +229,7 @@ def test_start_no_data(mocker, hyperopt_conf) -> None:
args = [ args = [
'hyperopt', 'hyperopt',
'--config', 'config.json', '--config', 'config.json',
'--hyperopt', 'DefaultHyperOpt', '--hyperopt', 'HyperoptTestSepFile',
'--hyperopt-loss', 'SharpeHyperOptLossDaily', '--hyperopt-loss', 'SharpeHyperOptLossDaily',
'--epochs', '5' '--epochs', '5'
] ]
@ -247,7 +247,7 @@ def test_start_filelock(mocker, hyperopt_conf, caplog) -> None:
args = [ args = [
'hyperopt', 'hyperopt',
'--config', 'config.json', '--config', 'config.json',
'--hyperopt', 'DefaultHyperOpt', '--hyperopt', 'HyperoptTestSepFile',
'--hyperopt-loss', 'SharpeHyperOptLossDaily', '--hyperopt-loss', 'SharpeHyperOptLossDaily',
'--epochs', '5' '--epochs', '5'
] ]
@ -351,7 +351,7 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
del hyperopt_conf['timeframe'] del hyperopt_conf['timeframe']
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start() hyperopt.start()
@ -426,7 +426,7 @@ def test_hyperopt_format_results(hyperopt):
def test_populate_indicators(hyperopt, testdatadir) -> None: def test_populate_indicators(hyperopt, testdatadir) -> None:
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data) dataframes = hyperopt.backtesting.strategy.advise_all_indicators(data)
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
{'pair': 'UNITTEST/BTC'}) {'pair': 'UNITTEST/BTC'})
@ -438,7 +438,7 @@ def test_populate_indicators(hyperopt, testdatadir) -> None:
def test_buy_strategy_generator(hyperopt, testdatadir) -> None: def test_buy_strategy_generator(hyperopt, testdatadir) -> None:
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data) dataframes = hyperopt.backtesting.strategy.advise_all_indicators(data)
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
{'pair': 'UNITTEST/BTC'}) {'pair': 'UNITTEST/BTC'})
@ -463,7 +463,7 @@ def test_buy_strategy_generator(hyperopt, testdatadir) -> None:
def test_sell_strategy_generator(hyperopt, testdatadir) -> None: def test_sell_strategy_generator(hyperopt, testdatadir) -> None:
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data) dataframes = hyperopt.backtesting.strategy.advise_all_indicators(data)
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
{'pair': 'UNITTEST/BTC'}) {'pair': 'UNITTEST/BTC'})
@ -660,7 +660,7 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
}) })
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start() hyperopt.start()
@ -713,7 +713,7 @@ def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None:
hyperopt_conf.update({'print_json': True}) hyperopt_conf.update({'print_json': True})
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start() hyperopt.start()
@ -761,7 +761,7 @@ def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
}) })
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start() hyperopt.start()
@ -805,7 +805,7 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non
hyperopt_conf.update({'spaces': 'roi stoploss'}) hyperopt_conf.update({'spaces': 'roi stoploss'})
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
del hyperopt.custom_hyperopt.__class__.buy_strategy_generator del hyperopt.custom_hyperopt.__class__.buy_strategy_generator
@ -844,7 +844,7 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None:
hyperopt_conf.update({'spaces': 'all', }) hyperopt_conf.update({'spaces': 'all', })
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
del hyperopt.custom_hyperopt.__class__.buy_strategy_generator del hyperopt.custom_hyperopt.__class__.buy_strategy_generator
@ -886,7 +886,7 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
hyperopt_conf.update({'spaces': 'buy'}) hyperopt_conf.update({'spaces': 'buy'})
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
# TODO: sell_strategy_generator() is actually not called because # TODO: sell_strategy_generator() is actually not called because
@ -940,7 +940,7 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
hyperopt_conf.update({'spaces': 'sell', }) hyperopt_conf.update({'spaces': 'sell', })
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
# TODO: buy_strategy_generator() is actually not called because # TODO: buy_strategy_generator() is actually not called because
@ -985,7 +985,7 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, method, space) -> No
hyperopt_conf.update({'spaces': space}) hyperopt_conf.update({'spaces': space})
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
delattr(hyperopt.custom_hyperopt.__class__, method) delattr(hyperopt.custom_hyperopt.__class__, method)

View File

@ -10,7 +10,7 @@ import rapidjson
from freqtrade.constants import FTHYPT_FILEVERSION from freqtrade.constants import FTHYPT_FILEVERSION
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer
from tests.conftest import log_has, log_has_re from tests.conftest import log_has
# Functions for recurrent object patching # Functions for recurrent object patching
@ -20,9 +20,14 @@ def create_results() -> List[Dict]:
def test_save_results_saves_epochs(hyperopt, tmpdir, caplog) -> None: def test_save_results_saves_epochs(hyperopt, tmpdir, caplog) -> None:
hyperopt.results_file = Path(tmpdir / 'ut_results.fthypt')
hyperopt_epochs = HyperoptTools.load_filtered_results(hyperopt.results_file, {})
assert hyperopt_epochs == ([], 0)
# Test writing to temp dir and reading again # Test writing to temp dir and reading again
epochs = create_results() epochs = create_results()
hyperopt.results_file = Path(tmpdir / 'ut_results.fthypt')
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
@ -33,68 +38,79 @@ def test_save_results_saves_epochs(hyperopt, tmpdir, caplog) -> None:
hyperopt._save_result(epochs[0]) hyperopt._save_result(epochs[0])
assert log_has(f"2 epochs saved to '{hyperopt.results_file}'.", caplog) assert log_has(f"2 epochs saved to '{hyperopt.results_file}'.", caplog)
hyperopt_epochs = HyperoptTools.load_previous_results(hyperopt.results_file) hyperopt_epochs = HyperoptTools.load_filtered_results(hyperopt.results_file, {})
assert len(hyperopt_epochs) == 2 assert len(hyperopt_epochs) == 2
assert hyperopt_epochs[1] == 2
assert len(hyperopt_epochs[0]) == 2
result_gen = HyperoptTools._read_results(hyperopt.results_file, 1)
def test_load_previous_results(testdatadir, caplog) -> None: epoch = next(result_gen)
assert len(epoch) == 1
results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle' assert epoch[0] == epochs[0]
epoch = next(result_gen)
hyperopt_epochs = HyperoptTools.load_previous_results(results_file) assert len(epoch) == 1
epoch = next(result_gen)
assert len(hyperopt_epochs) == 5 assert len(epoch) == 0
assert log_has_re(r"Reading pickled epochs from .*", caplog) with pytest.raises(StopIteration):
next(result_gen)
caplog.clear()
# Modern version
results_file = testdatadir / 'strategy_SampleStrategy.fthypt'
hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
assert len(hyperopt_epochs) == 5
assert log_has_re(r"Reading epochs from .*", caplog)
def test_load_previous_results2(mocker, testdatadir, caplog) -> None: def test_load_previous_results2(mocker, testdatadir, caplog) -> None:
mocker.patch('freqtrade.optimize.hyperopt_tools.HyperoptTools._read_results_pickle',
return_value=[{'asdf': '222'}])
results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle' results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle'
with pytest.raises(OperationalException, match=r"The file .* incompatible.*"): with pytest.raises(OperationalException,
HyperoptTools.load_previous_results(results_file) match=r"Legacy hyperopt results are no longer supported.*"):
HyperoptTools.load_filtered_results(results_file, {})
@pytest.mark.parametrize("spaces, expected_results", [ @pytest.mark.parametrize("spaces, expected_results", [
(['buy'], (['buy'],
{'buy': True, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False}), {'buy': True, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
'protection': False}),
(['sell'], (['sell'],
{'buy': False, 'sell': True, 'roi': False, 'stoploss': False, 'trailing': False}), {'buy': False, 'sell': True, 'roi': False, 'stoploss': False, 'trailing': False,
'protection': False}),
(['roi'], (['roi'],
{'buy': False, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False}), {'buy': False, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False,
'protection': False}),
(['stoploss'], (['stoploss'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': True, 'trailing': False}), {'buy': False, 'sell': False, 'roi': False, 'stoploss': True, 'trailing': False,
'protection': False}),
(['trailing'], (['trailing'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': True}), {'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': True,
'protection': False}),
(['buy', 'sell', 'roi', 'stoploss'], (['buy', 'sell', 'roi', 'stoploss'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}), {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
'protection': False}),
(['buy', 'sell', 'roi', 'stoploss', 'trailing'], (['buy', 'sell', 'roi', 'stoploss', 'trailing'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}), {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
'protection': False}),
(['buy', 'roi'], (['buy', 'roi'],
{'buy': True, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False}), {'buy': True, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False,
'protection': False}),
(['all'], (['all'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}), {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
'protection': True}),
(['default'], (['default'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}), {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
'protection': False}),
(['default', 'trailing'], (['default', 'trailing'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}), {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
'protection': False}),
(['all', 'buy'], (['all', 'buy'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}), {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
'protection': True}),
(['default', 'buy'], (['default', 'buy'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}), {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
'protection': False}),
(['all'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
'protection': True}),
(['protection'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
'protection': True}),
]) ])
def test_has_space(hyperopt_conf, spaces, expected_results): def test_has_space(hyperopt_conf, spaces, expected_results):
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']: for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection']:
hyperopt_conf.update({'spaces': spaces}) hyperopt_conf.update({'spaces': spaces})
assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s] assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s]
@ -151,9 +167,9 @@ def test__pprint_dict():
def test_get_strategy_filename(default_conf): def test_get_strategy_filename(default_conf):
x = HyperoptTools.get_strategy_filename(default_conf, 'DefaultStrategy') x = HyperoptTools.get_strategy_filename(default_conf, 'StrategyTestV2')
assert isinstance(x, Path) assert isinstance(x, Path)
assert x == Path(__file__).parents[1] / 'strategy/strats/default_strategy.py' assert x == Path(__file__).parents[1] / 'strategy/strats/strategy_test_v2.py'
x = HyperoptTools.get_strategy_filename(default_conf, 'NonExistingStrategy') x = HyperoptTools.get_strategy_filename(default_conf, 'NonExistingStrategy')
assert x is None assert x is None
@ -161,7 +177,7 @@ def test_get_strategy_filename(default_conf):
def test_export_params(tmpdir): def test_export_params(tmpdir):
filename = Path(tmpdir) / "DefaultStrategy.json" filename = Path(tmpdir) / "StrategyTestV2.json"
assert not filename.is_file() assert not filename.is_file()
params = { params = {
"params_details": { "params_details": {
@ -189,12 +205,12 @@ def test_export_params(tmpdir):
} }
} }
HyperoptTools.export_params(params, "DefaultStrategy", filename) HyperoptTools.export_params(params, "StrategyTestV2", filename)
assert filename.is_file() assert filename.is_file()
content = rapidjson.load(filename.open('r')) content = rapidjson.load(filename.open('r'))
assert content['strategy_name'] == 'DefaultStrategy' assert content['strategy_name'] == 'StrategyTestV2'
assert 'params' in content assert 'params' in content
assert "buy" in content["params"] assert "buy" in content["params"]
assert "sell" in content["params"] assert "sell" in content["params"]
@ -207,7 +223,7 @@ def test_try_export_params(default_conf, tmpdir, caplog, mocker):
default_conf['disableparamexport'] = False default_conf['disableparamexport'] = False
export_mock = mocker.patch("freqtrade.optimize.hyperopt_tools.HyperoptTools.export_params") export_mock = mocker.patch("freqtrade.optimize.hyperopt_tools.HyperoptTools.export_params")
filename = Path(tmpdir) / "DefaultStrategy.json" filename = Path(tmpdir) / "StrategyTestV2.json"
assert not filename.is_file() assert not filename.is_file()
params = { params = {
"params_details": { "params_details": {
@ -236,17 +252,17 @@ def test_try_export_params(default_conf, tmpdir, caplog, mocker):
FTHYPT_FILEVERSION: 2, FTHYPT_FILEVERSION: 2,
} }
HyperoptTools.try_export_params(default_conf, "DefaultStrategy22", params) HyperoptTools.try_export_params(default_conf, "StrategyTestV222", params)
assert log_has("Strategy not found, not exporting parameter file.", caplog) assert log_has("Strategy not found, not exporting parameter file.", caplog)
assert export_mock.call_count == 0 assert export_mock.call_count == 0
caplog.clear() caplog.clear()
HyperoptTools.try_export_params(default_conf, "DefaultStrategy", params) HyperoptTools.try_export_params(default_conf, "StrategyTestV2", params)
assert export_mock.call_count == 1 assert export_mock.call_count == 1
assert export_mock.call_args_list[0][0][1] == 'DefaultStrategy' assert export_mock.call_args_list[0][0][1] == 'StrategyTestV2'
assert export_mock.call_args_list[0][0][2].name == 'default_strategy.json' assert export_mock.call_args_list[0][0][2].name == 'strategy_test_v2.json'
def test_params_print(capsys): def test_params_print(capsys):

View File

@ -4,7 +4,7 @@ from unittest.mock import MagicMock
import pytest import pytest
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.optimize.default_hyperopt_loss import ShortTradeDurHyperOptLoss from freqtrade.optimize.hyperopt_loss_short_trade_dur import ShortTradeDurHyperOptLoss
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver

View File

@ -52,7 +52,7 @@ def test_text_table_bt_results():
def test_generate_backtest_stats(default_conf, testdatadir, tmpdir): def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'StrategyTestV2'})
StrategyResolver.load_strategy(default_conf) StrategyResolver.load_strategy(default_conf)
results = {'DefStrat': { results = {'DefStrat': {

View File

@ -22,7 +22,7 @@ def test_fiat_convert_is_supported(mocker):
def test_fiat_convert_find_price(mocker): def test_fiat_convert_find_price(mocker):
fiat_convert = CryptoToFiatConverter() fiat_convert = CryptoToFiatConverter()
fiat_convert._cryptomap = {} fiat_convert._coinlistings = {}
fiat_convert._backoff = 0 fiat_convert._backoff = 0
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._load_cryptomap', mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._load_cryptomap',
return_value=None) return_value=None)
@ -44,7 +44,7 @@ def test_fiat_convert_find_price(mocker):
def test_fiat_convert_unsupported_crypto(mocker, caplog): def test_fiat_convert_unsupported_crypto(mocker, caplog):
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[]) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._coinlistings', return_value=[])
fiat_convert = CryptoToFiatConverter() fiat_convert = CryptoToFiatConverter()
assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0 assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0
assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog) assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog)
@ -88,9 +88,9 @@ def test_fiat_convert_two_FIAT(mocker):
def test_loadcryptomap(mocker): def test_loadcryptomap(mocker):
fiat_convert = CryptoToFiatConverter() fiat_convert = CryptoToFiatConverter()
assert len(fiat_convert._cryptomap) == 2 assert len(fiat_convert._coinlistings) == 2
assert fiat_convert._cryptomap["btc"] == "bitcoin" assert fiat_convert._get_gekko_id("btc") == "bitcoin"
def test_fiat_init_network_exception(mocker): def test_fiat_init_network_exception(mocker):
@ -102,11 +102,10 @@ def test_fiat_init_network_exception(mocker):
) )
# with pytest.raises(RequestEsxception): # with pytest.raises(RequestEsxception):
fiat_convert = CryptoToFiatConverter() fiat_convert = CryptoToFiatConverter()
fiat_convert._cryptomap = {} fiat_convert._coinlistings = {}
fiat_convert._load_cryptomap() fiat_convert._load_cryptomap()
length_cryptomap = len(fiat_convert._cryptomap) assert len(fiat_convert._coinlistings) == 0
assert length_cryptomap == 0
def test_fiat_convert_without_network(mocker): def test_fiat_convert_without_network(mocker):
@ -132,11 +131,10 @@ def test_fiat_too_many_requests_response(mocker, caplog):
) )
# with pytest.raises(RequestEsxception): # with pytest.raises(RequestEsxception):
fiat_convert = CryptoToFiatConverter() fiat_convert = CryptoToFiatConverter()
fiat_convert._cryptomap = {} fiat_convert._coinlistings = {}
fiat_convert._load_cryptomap() fiat_convert._load_cryptomap()
length_cryptomap = len(fiat_convert._cryptomap) assert len(fiat_convert._coinlistings) == 0
assert length_cryptomap == 0
assert fiat_convert._backoff > datetime.datetime.now().timestamp() assert fiat_convert._backoff > datetime.datetime.now().timestamp()
assert log_has( assert log_has(
'Too many requests for Coingecko API, backing off and trying again later.', 'Too many requests for Coingecko API, backing off and trying again later.',
@ -144,20 +142,33 @@ def test_fiat_too_many_requests_response(mocker, caplog):
) )
def test_fiat_multiple_coins(mocker, caplog):
fiat_convert = CryptoToFiatConverter()
fiat_convert._coinlistings = [
{'id': 'helium', 'symbol': 'hnt', 'name': 'Helium'},
{'id': 'hymnode', 'symbol': 'hnt', 'name': 'Hymnode'},
{'id': 'bitcoin', 'symbol': 'btc', 'name': 'Bitcoin'},
]
assert fiat_convert._get_gekko_id('btc') == 'bitcoin'
assert fiat_convert._get_gekko_id('hnt') is None
assert log_has('Found multiple mappings in goingekko for hnt.', caplog)
def test_fiat_invalid_response(mocker, caplog): def test_fiat_invalid_response(mocker, caplog):
# Because CryptoToFiatConverter is a Singleton we reset the listings # Because CryptoToFiatConverter is a Singleton we reset the listings
listmock = MagicMock(return_value="{'novalidjson':DEADBEEFf}") listmock = MagicMock(return_value=None)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.fiat_convert.CoinGeckoAPI', 'freqtrade.rpc.fiat_convert.CoinGeckoAPI',
get_coins_list=listmock, get_coins_list=listmock,
) )
# with pytest.raises(RequestEsxception): # with pytest.raises(RequestEsxception):
fiat_convert = CryptoToFiatConverter() fiat_convert = CryptoToFiatConverter()
fiat_convert._cryptomap = {} fiat_convert._coinlistings = []
fiat_convert._load_cryptomap() fiat_convert._load_cryptomap()
length_cryptomap = len(fiat_convert._cryptomap) assert len(fiat_convert._coinlistings) == 0
assert length_cryptomap == 0
assert log_has_re('Could not load FIAT Cryptocurrency map for the following problem: .*', assert log_has_re('Could not load FIAT Cryptocurrency map for the following problem: .*',
caplog) caplog)

View File

@ -35,7 +35,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
) )
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
@ -200,7 +200,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
) )
del default_conf['fiat_display_currency'] del default_conf['fiat_display_currency']
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
@ -247,7 +247,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
) )
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
stake_currency = default_conf['stake_currency'] stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency'] fiat_display_currency = default_conf['fiat_display_currency']
@ -379,7 +379,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
) )
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
stake_currency = default_conf['stake_currency'] stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency'] fiat_display_currency = default_conf['fiat_display_currency']
@ -467,7 +467,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
) )
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
stake_currency = default_conf['stake_currency'] stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency'] fiat_display_currency = default_conf['fiat_display_currency']
@ -534,7 +534,7 @@ def test_rpc_balance_handle_error(default_conf, mocker):
) )
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter() rpc._fiat_converter = CryptoToFiatConverter()
with pytest.raises(RPCException, match="Error getting current tickers."): with pytest.raises(RPCException, match="Error getting current tickers."):
@ -575,7 +575,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers):
) )
default_conf['dry_run'] = False default_conf['dry_run'] = False
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter() rpc._fiat_converter = CryptoToFiatConverter()
@ -620,7 +620,7 @@ def test_rpc_start(mocker, default_conf) -> None:
) )
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
@ -641,7 +641,7 @@ def test_rpc_stop(mocker, default_conf) -> None:
) )
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
@ -663,7 +663,7 @@ def test_rpc_stopbuy(mocker, default_conf) -> None:
) )
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
@ -695,7 +695,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=1000) mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=1000)
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
@ -813,7 +813,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
) )
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
# Create some test data # Create some test data
@ -846,7 +846,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
) )
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
counts = rpc._rpc_count() counts = rpc._rpc_count()
@ -871,7 +871,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) ->
) )
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
pair = 'ETH/BTC' pair = 'ETH/BTC'
trade = rpc._rpc_forcebuy(pair, None) trade = rpc._rpc_forcebuy(pair, None)
@ -897,7 +897,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) ->
# Test not buying # Test not buying
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
freqtradebot.config['stake_amount'] = 0 freqtradebot.config['stake_amount'] = 0
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
pair = 'TKN/BTC' pair = 'TKN/BTC'
trade = rpc._rpc_forcebuy(pair, None) trade = rpc._rpc_forcebuy(pair, None)
@ -910,7 +910,7 @@ def test_rpcforcebuy_stopped(mocker, default_conf) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
pair = 'ETH/BTC' pair = 'ETH/BTC'
with pytest.raises(RPCException, match=r'trader is not running'): with pytest.raises(RPCException, match=r'trader is not running'):
@ -921,7 +921,7 @@ def test_rpcforcebuy_disabled(mocker, default_conf) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
pair = 'ETH/BTC' pair = 'ETH/BTC'
with pytest.raises(RPCException, match=r'Forcebuy not enabled.'): with pytest.raises(RPCException, match=r'Forcebuy not enabled.'):

View File

@ -109,6 +109,11 @@ def test_api_ui_fallback(botclient):
rc = client_get(client, "/something") rc = client_get(client, "/something")
assert rc.status_code == 200 assert rc.status_code == 200
# Test directory traversal
rc = client_get(client, '%2F%2F%2Fetc/passwd')
assert rc.status_code == 200
assert '`freqtrade install-ui`' in rc.text
def test_api_ui_version(botclient, mocker): def test_api_ui_version(botclient, mocker):
ftbot, client = botclient ftbot, client = botclient
@ -442,7 +447,7 @@ def test_api_balance(botclient, mocker, rpc_balance):
def test_api_count(botclient, mocker, ticker, fee, markets): def test_api_count(botclient, mocker, ticker, fee, markets):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot, (True, False, None)) patch_get_signal(ftbot)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
get_balances=MagicMock(return_value=ticker), get_balances=MagicMock(return_value=ticker),
@ -504,7 +509,7 @@ def test_api_locks(botclient):
def test_api_show_config(botclient, mocker): def test_api_show_config(botclient, mocker):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot, (True, False, None)) patch_get_signal(ftbot)
rc = client_get(client, f"{BASE_URI}/show_config") rc = client_get(client, f"{BASE_URI}/show_config")
assert_response(rc) assert_response(rc)
@ -522,7 +527,7 @@ def test_api_show_config(botclient, mocker):
def test_api_daily(botclient, mocker, ticker, fee, markets): def test_api_daily(botclient, mocker, ticker, fee, markets):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot, (True, False, None)) patch_get_signal(ftbot)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
get_balances=MagicMock(return_value=ticker), get_balances=MagicMock(return_value=ticker),
@ -540,7 +545,7 @@ def test_api_daily(botclient, mocker, ticker, fee, markets):
def test_api_trades(botclient, mocker, fee, markets): def test_api_trades(botclient, mocker, fee, markets):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot, (True, False, None)) patch_get_signal(ftbot)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets) markets=PropertyMock(return_value=markets)
@ -568,7 +573,7 @@ def test_api_trades(botclient, mocker, fee, markets):
def test_api_trade_single(botclient, mocker, fee, ticker, markets): def test_api_trade_single(botclient, mocker, fee, ticker, markets):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot, (True, False, None)) patch_get_signal(ftbot)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets), markets=PropertyMock(return_value=markets),
@ -588,7 +593,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets):
def test_api_delete_trade(botclient, mocker, fee, markets): def test_api_delete_trade(botclient, mocker, fee, markets):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot, (True, False, None)) patch_get_signal(ftbot)
stoploss_mock = MagicMock() stoploss_mock = MagicMock()
cancel_mock = MagicMock() cancel_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
@ -662,7 +667,7 @@ def test_api_logs(botclient):
def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot, (True, False, None)) patch_get_signal(ftbot)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
get_balances=MagicMock(return_value=ticker), get_balances=MagicMock(return_value=ticker),
@ -678,7 +683,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_api_profit(botclient, mocker, ticker, fee, markets): def test_api_profit(botclient, mocker, ticker, fee, markets):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot, (True, False, None)) patch_get_signal(ftbot)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
get_balances=MagicMock(return_value=ticker), get_balances=MagicMock(return_value=ticker),
@ -729,7 +734,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets):
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_api_stats(botclient, mocker, ticker, fee, markets,): def test_api_stats(botclient, mocker, ticker, fee, markets,):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot, (True, False, None)) patch_get_signal(ftbot)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
get_balances=MagicMock(return_value=ticker), get_balances=MagicMock(return_value=ticker),
@ -757,7 +762,7 @@ def test_api_stats(botclient, mocker, ticker, fee, markets,):
def test_api_performance(botclient, fee): def test_api_performance(botclient, fee):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot, (True, False, None)) patch_get_signal(ftbot)
trade = Trade( trade = Trade(
pair='LTC/ETH', pair='LTC/ETH',
@ -803,7 +808,7 @@ def test_api_performance(botclient, fee):
def test_api_status(botclient, mocker, ticker, fee, markets): def test_api_status(botclient, mocker, ticker, fee, markets):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot, (True, False, None)) patch_get_signal(ftbot)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
get_balances=MagicMock(return_value=ticker), get_balances=MagicMock(return_value=ticker),
@ -874,7 +879,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
'open_trade_value': 15.1668225, 'open_trade_value': 15.1668225,
'sell_reason': None, 'sell_reason': None,
'sell_order_status': None, 'sell_order_status': None,
'strategy': 'DefaultStrategy', 'strategy': 'StrategyTestV2',
'buy_tag': None, 'buy_tag': None,
'timeframe': 5, 'timeframe': 5,
'exchange': 'binance', 'exchange': 'binance',
@ -979,7 +984,7 @@ def test_api_forcebuy(botclient, mocker, fee):
close_rate=0.265441, close_rate=0.265441,
id=22, id=22,
timeframe=5, timeframe=5,
strategy="DefaultStrategy" strategy="StrategyTestV2"
)) ))
mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock)
@ -1029,7 +1034,7 @@ def test_api_forcebuy(botclient, mocker, fee):
'open_trade_value': 0.24605460, 'open_trade_value': 0.24605460,
'sell_reason': None, 'sell_reason': None,
'sell_order_status': None, 'sell_order_status': None,
'strategy': 'DefaultStrategy', 'strategy': 'StrategyTestV2',
'buy_tag': None, 'buy_tag': None,
'timeframe': 5, 'timeframe': 5,
'exchange': 'binance', 'exchange': 'binance',
@ -1046,7 +1051,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets):
markets=PropertyMock(return_value=markets), markets=PropertyMock(return_value=markets),
_is_dry_limit_order_filled=MagicMock(return_value=False), _is_dry_limit_order_filled=MagicMock(return_value=False),
) )
patch_get_signal(ftbot, (True, False, None)) patch_get_signal(ftbot)
rc = client_post(client, f"{BASE_URI}/forcesell", rc = client_post(client, f"{BASE_URI}/forcesell",
data='{"tradeid": "1"}') data='{"tradeid": "1"}')
@ -1096,7 +1101,7 @@ def test_api_pair_candles(botclient, ohlcv_history):
f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}")
assert_response(rc) assert_response(rc)
assert 'strategy' in rc.json() assert 'strategy' in rc.json()
assert rc.json()['strategy'] == 'DefaultStrategy' assert rc.json()['strategy'] == 'StrategyTestV2'
assert 'columns' in rc.json() assert 'columns' in rc.json()
assert 'data_start_ts' in rc.json() assert 'data_start_ts' in rc.json()
assert 'data_start' in rc.json() assert 'data_start' in rc.json()
@ -1134,19 +1139,19 @@ def test_api_pair_history(botclient, ohlcv_history):
# No pair # No pair
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_history?timeframe={timeframe}" f"{BASE_URI}/pair_history?timeframe={timeframe}"
"&timerange=20180111-20180112&strategy=DefaultStrategy") "&timerange=20180111-20180112&strategy=StrategyTestV2")
assert_response(rc, 422) assert_response(rc, 422)
# No Timeframe # No Timeframe
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC" f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC"
"&timerange=20180111-20180112&strategy=DefaultStrategy") "&timerange=20180111-20180112&strategy=StrategyTestV2")
assert_response(rc, 422) assert_response(rc, 422)
# No timerange # No timerange
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
"&strategy=DefaultStrategy") "&strategy=StrategyTestV2")
assert_response(rc, 422) assert_response(rc, 422)
# No strategy # No strategy
@ -1158,14 +1163,14 @@ def test_api_pair_history(botclient, ohlcv_history):
# Working # Working
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
"&timerange=20180111-20180112&strategy=DefaultStrategy") "&timerange=20180111-20180112&strategy=StrategyTestV2")
assert_response(rc, 200) assert_response(rc, 200)
assert rc.json()['length'] == 289 assert rc.json()['length'] == 289
assert len(rc.json()['data']) == rc.json()['length'] assert len(rc.json()['data']) == rc.json()['length']
assert 'columns' in rc.json() assert 'columns' in rc.json()
assert 'data' in rc.json() assert 'data' in rc.json()
assert rc.json()['pair'] == 'UNITTEST/BTC' assert rc.json()['pair'] == 'UNITTEST/BTC'
assert rc.json()['strategy'] == 'DefaultStrategy' assert rc.json()['strategy'] == 'StrategyTestV2'
assert rc.json()['data_start'] == '2018-01-11 00:00:00+00:00' assert rc.json()['data_start'] == '2018-01-11 00:00:00+00:00'
assert rc.json()['data_start_ts'] == 1515628800000 assert rc.json()['data_start_ts'] == 1515628800000
assert rc.json()['data_stop'] == '2018-01-12 00:00:00+00:00' assert rc.json()['data_stop'] == '2018-01-12 00:00:00+00:00'
@ -1174,7 +1179,7 @@ def test_api_pair_history(botclient, ohlcv_history):
# No data found # No data found
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
"&timerange=20200111-20200112&strategy=DefaultStrategy") "&timerange=20200111-20200112&strategy=StrategyTestV2")
assert_response(rc, 502) assert_response(rc, 502)
assert rc.json()['error'] == ("Error querying /api/v1/pair_history: " assert rc.json()['error'] == ("Error querying /api/v1/pair_history: "
"No data for UNITTEST/BTC, 5m in 20200111-20200112 found.") "No data for UNITTEST/BTC, 5m in 20200111-20200112 found.")
@ -1212,21 +1217,21 @@ def test_api_strategies(botclient):
assert_response(rc) assert_response(rc)
assert rc.json() == {'strategies': [ assert rc.json() == {'strategies': [
'DefaultStrategy',
'HyperoptableStrategy', 'HyperoptableStrategy',
'TestStrategyLegacy' 'StrategyTestV2',
'TestStrategyLegacyV1'
]} ]}
def test_api_strategy(botclient): def test_api_strategy(botclient):
ftbot, client = botclient ftbot, client = botclient
rc = client_get(client, f"{BASE_URI}/strategy/DefaultStrategy") rc = client_get(client, f"{BASE_URI}/strategy/StrategyTestV2")
assert_response(rc) assert_response(rc)
assert rc.json()['strategy'] == 'DefaultStrategy' assert rc.json()['strategy'] == 'StrategyTestV2'
data = (Path(__file__).parents[1] / "strategy/strats/default_strategy.py").read_text() data = (Path(__file__).parents[1] / "strategy/strats/strategy_test_v2.py").read_text()
assert rc.json()['code'] == data assert rc.json()['code'] == data
rc = client_get(client, f"{BASE_URI}/strategy/NoStrat") rc = client_get(client, f"{BASE_URI}/strategy/NoStrat")
@ -1283,7 +1288,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog):
# start backtesting # start backtesting
data = { data = {
"strategy": "DefaultStrategy", "strategy": "StrategyTestV2",
"timeframe": "5m", "timeframe": "5m",
"timerange": "20180110-20180111", "timerange": "20180110-20180111",
"max_open_trades": 3, "max_open_trades": 3,

View File

@ -119,7 +119,7 @@ def test_authorized_only(default_conf, mocker, caplog, update) -> None:
rpc = RPC(bot) rpc = RPC(bot)
dummy = DummyCls(rpc, default_conf) dummy = DummyCls(rpc, default_conf)
patch_get_signal(bot, (True, False, None)) patch_get_signal(bot)
dummy.dummy_handler(update=update, context=MagicMock()) dummy.dummy_handler(update=update, context=MagicMock())
assert dummy.state['called'] is True assert dummy.state['called'] is True
assert log_has('Executing handler: dummy_handler for chat_id: 0', caplog) assert log_has('Executing handler: dummy_handler for chat_id: 0', caplog)
@ -139,7 +139,7 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
rpc = RPC(bot) rpc = RPC(bot)
dummy = DummyCls(rpc, default_conf) dummy = DummyCls(rpc, default_conf)
patch_get_signal(bot, (True, False, None)) patch_get_signal(bot)
dummy.dummy_handler(update=update, context=MagicMock()) dummy.dummy_handler(update=update, context=MagicMock())
assert dummy.state['called'] is False assert dummy.state['called'] is False
assert not log_has('Executing handler: dummy_handler for chat_id: 3735928559', caplog) assert not log_has('Executing handler: dummy_handler for chat_id: 3735928559', caplog)
@ -155,7 +155,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None
bot = FreqtradeBot(default_conf) bot = FreqtradeBot(default_conf)
rpc = RPC(bot) rpc = RPC(bot)
dummy = DummyCls(rpc, default_conf) dummy = DummyCls(rpc, default_conf)
patch_get_signal(bot, (True, False, None)) patch_get_signal(bot)
dummy.dummy_exception(update=update, context=MagicMock()) dummy.dummy_exception(update=update, context=MagicMock())
assert dummy.state['called'] is False assert dummy.state['called'] is False
@ -229,7 +229,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
# Status is also enabled when stopped # Status is also enabled when stopped
@ -286,7 +286,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
# Status table is also enabled when stopped # Status table is also enabled when stopped
@ -330,7 +330,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
# Create some test data # Create some test data
freqtradebot.enter_positions() freqtradebot.enter_positions()
@ -401,7 +401,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
) )
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
# Try invalid data # Try invalid data
msg_mock.reset_mock() msg_mock.reset_mock()
@ -433,7 +433,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
) )
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
telegram._profit(update=update, context=MagicMock()) telegram._profit(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
@ -488,7 +488,7 @@ def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee,
get_fee=fee, get_fee=fee,
) )
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
telegram._stats(update=update, context=MagicMock()) telegram._stats(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
@ -514,7 +514,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick
side_effect=lambda a, b: f"{a}/{b}") side_effect=lambda a, b: f"{a}/{b}")
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
telegram._balance(update=update, context=MagicMock()) telegram._balance(update=update, context=MagicMock())
result = msg_mock.call_args_list[0][0][0] result = msg_mock.call_args_list[0][0][0]
@ -537,7 +537,7 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={})
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
freqtradebot.config['dry_run'] = False freqtradebot.config['dry_run'] = False
telegram._balance(update=update, context=MagicMock()) telegram._balance(update=update, context=MagicMock())
@ -550,7 +550,7 @@ def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={})
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
telegram._balance(update=update, context=MagicMock()) telegram._balance(update=update, context=MagicMock())
result = msg_mock.call_args_list[0][0][0] result = msg_mock.call_args_list[0][0][0]
@ -579,7 +579,7 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None
}) })
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
telegram._balance(update=update, context=MagicMock()) telegram._balance(update=update, context=MagicMock())
assert msg_mock.call_count > 1 assert msg_mock.call_count > 1
@ -678,7 +678,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
telegram = Telegram(rpc, default_conf) telegram = Telegram(rpc, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
# Create some test data # Create some test data
freqtradebot.enter_positions() freqtradebot.enter_positions()
@ -737,7 +737,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
telegram = Telegram(rpc, default_conf) telegram = Telegram(rpc, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
# Create some test data # Create some test data
freqtradebot.enter_positions() freqtradebot.enter_positions()
@ -798,7 +798,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
telegram = Telegram(rpc, default_conf) telegram = Telegram(rpc, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
# Create some test data # Create some test data
freqtradebot.enter_positions() freqtradebot.enter_positions()
@ -839,7 +839,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
return_value=15000.0) return_value=15000.0)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
# Trader is not running # Trader is not running
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
@ -877,7 +877,7 @@ def test_forcebuy_handle(default_conf, update, mocker) -> None:
mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock) mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock)
telegram, freqtradebot, _ = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, _ = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
# /forcebuy ETH/BTC # /forcebuy ETH/BTC
context = MagicMock() context = MagicMock()
@ -906,7 +906,7 @@ def test_forcebuy_handle_exception(default_conf, update, mocker) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
update.message.text = '/forcebuy ETH/Nonepair' update.message.text = '/forcebuy ETH/Nonepair'
telegram._forcebuy(update=update, context=MagicMock()) telegram._forcebuy(update=update, context=MagicMock())
@ -923,7 +923,7 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None:
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
context = MagicMock() context = MagicMock()
context.args = [] context.args = []
@ -951,7 +951,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
get_fee=fee, get_fee=fee,
) )
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
# Create some test data # Create some test data
freqtradebot.enter_positions() freqtradebot.enter_positions()
@ -979,7 +979,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
get_fee=fee, get_fee=fee,
) )
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
telegram._count(update=update, context=MagicMock()) telegram._count(update=update, context=MagicMock())
@ -1008,7 +1008,7 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None
get_fee=fee, get_fee=fee,
) )
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False, None)) patch_get_signal(freqtradebot)
telegram._locks(update=update, context=MagicMock()) telegram._locks(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'No active locks.' in msg_mock.call_args_list[0][0][0] assert 'No active locks.' in msg_mock.call_args_list[0][0][0]
@ -1236,7 +1236,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0]
assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0]
assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0] assert '*Strategy:* `StrategyTestV2`' in msg_mock.call_args_list[0][0][0]
assert '*Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] assert '*Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
@ -1245,7 +1245,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0]
assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0]
assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0] assert '*Strategy:* `StrategyTestV2`' in msg_mock.call_args_list[0][0][0]
assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0]

View File

@ -5,5 +5,5 @@ import nonexiting_module # noqa
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
class TestStrategyLegacy(IStrategy): class TestStrategyLegacyV1(IStrategy):
pass pass

View File

@ -10,7 +10,7 @@ from freqtrade.strategy.interface import IStrategy
# -------------------------------- # --------------------------------
# This class is a sample. Feel free to customize it. # This class is a sample. Feel free to customize it.
class TestStrategyLegacy(IStrategy): class TestStrategyLegacyV1(IStrategy):
""" """
This is a test strategy using the legacy function headers, which will be This is a test strategy using the legacy function headers, which will be
removed in a future update. removed in a future update.

View File

@ -7,9 +7,9 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
class DefaultStrategy(IStrategy): class StrategyTestV2(IStrategy):
""" """
Default Strategy provided by freqtrade bot. Strategy used by tests freqtrade bot.
Please do not modify this strategy, it's intended for internal use only. Please do not modify this strategy, it's intended for internal use only.
Please look at the SampleStrategy in the user_data/strategy directory Please look at the SampleStrategy in the user_data/strategy directory
or strategy repository https://github.com/freqtrade/freqtrade-strategies or strategy repository https://github.com/freqtrade/freqtrade-strategies

View File

@ -4,20 +4,20 @@ from pandas import DataFrame
from freqtrade.persistence.models import Trade from freqtrade.persistence.models import Trade
from .strats.default_strategy import DefaultStrategy from .strats.strategy_test_v2 import StrategyTestV2
def test_default_strategy_structure(): def test_strategy_test_v2_structure():
assert hasattr(DefaultStrategy, 'minimal_roi') assert hasattr(StrategyTestV2, 'minimal_roi')
assert hasattr(DefaultStrategy, 'stoploss') assert hasattr(StrategyTestV2, 'stoploss')
assert hasattr(DefaultStrategy, 'timeframe') assert hasattr(StrategyTestV2, 'timeframe')
assert hasattr(DefaultStrategy, 'populate_indicators') assert hasattr(StrategyTestV2, 'populate_indicators')
assert hasattr(DefaultStrategy, 'populate_buy_trend') assert hasattr(StrategyTestV2, 'populate_buy_trend')
assert hasattr(DefaultStrategy, 'populate_sell_trend') assert hasattr(StrategyTestV2, 'populate_sell_trend')
def test_default_strategy(result, fee): def test_strategy_test_v2(result, fee):
strategy = DefaultStrategy({}) strategy = StrategyTestV2({})
metadata = {'pair': 'ETH/BTC'} metadata = {'pair': 'ETH/BTC'}
assert type(strategy.minimal_roi) is dict assert type(strategy.minimal_roi) is dict

View File

@ -22,11 +22,11 @@ from freqtrade.strategy.interface import SellCheckTuple
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from tests.conftest import log_has, log_has_re from tests.conftest import log_has, log_has_re
from .strats.default_strategy import DefaultStrategy from .strats.strategy_test_v2 import StrategyTestV2
# Avoid to reinit the same object again and again # Avoid to reinit the same object again and again
_STRATEGY = DefaultStrategy(config={}) _STRATEGY = StrategyTestV2(config={})
_STRATEGY.dp = DataProvider({}, None, None) _STRATEGY.dp = DataProvider({}, None, None)
@ -148,7 +148,7 @@ def test_get_signal_no_sell_column(default_conf, mocker, caplog, ohlcv_history):
def test_ignore_expired_candle(default_conf): def test_ignore_expired_candle(default_conf):
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
strategy.ignore_buying_expired_candle_after = 60 strategy.ignore_buying_expired_candle_after = 60
@ -232,25 +232,25 @@ def test_assert_df(ohlcv_history, caplog):
_STRATEGY.disable_dataframe_checks = False _STRATEGY.disable_dataframe_checks = False
def test_ohlcvdata_to_dataframe(default_conf, testdatadir) -> None: def test_advise_all_indicators(default_conf, testdatadir) -> None:
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
timerange = TimeRange.parse_timerange('1510694220-1510700340') timerange = TimeRange.parse_timerange('1510694220-1510700340')
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
fill_up_missing=True) fill_up_missing=True)
processed = strategy.ohlcvdata_to_dataframe(data) processed = strategy.advise_all_indicators(data)
assert len(processed['UNITTEST/BTC']) == 102 # partial candle was removed assert len(processed['UNITTEST/BTC']) == 102 # partial candle was removed
def test_ohlcvdata_to_dataframe_copy(mocker, default_conf, testdatadir) -> None: def test_advise_all_indicators_copy(mocker, default_conf, testdatadir) -> None:
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
aimock = mocker.patch('freqtrade.strategy.interface.IStrategy.advise_indicators') aimock = mocker.patch('freqtrade.strategy.interface.IStrategy.advise_indicators')
timerange = TimeRange.parse_timerange('1510694220-1510700340') timerange = TimeRange.parse_timerange('1510694220-1510700340')
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
fill_up_missing=True) fill_up_missing=True)
strategy.ohlcvdata_to_dataframe(data) strategy.advise_all_indicators(data)
assert aimock.call_count == 1 assert aimock.call_count == 1
# Ensure that a copy of the dataframe is passed to advice_indicators # Ensure that a copy of the dataframe is passed to advice_indicators
assert aimock.call_args_list[0][0][0] is not data assert aimock.call_args_list[0][0][0] is not data
@ -262,7 +262,7 @@ def test_min_roi_reached(default_conf, fee) -> None:
min_roi_list = [{20: 0.05, 55: 0.01, 0: 0.1}, min_roi_list = [{20: 0.05, 55: 0.01, 0: 0.1},
{0: 0.1, 20: 0.05, 55: 0.01}] {0: 0.1, 20: 0.05, 55: 0.01}]
for roi in min_roi_list: for roi in min_roi_list:
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
strategy.minimal_roi = roi strategy.minimal_roi = roi
trade = Trade( trade = Trade(
@ -301,7 +301,7 @@ def test_min_roi_reached2(default_conf, fee) -> None:
}, },
] ]
for roi in min_roi_list: for roi in min_roi_list:
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
strategy.minimal_roi = roi strategy.minimal_roi = roi
trade = Trade( trade = Trade(
@ -336,7 +336,7 @@ def test_min_roi_reached3(default_conf, fee) -> None:
30: 0.05, 30: 0.05,
55: 0.30, 55: 0.30,
} }
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
strategy.minimal_roi = min_roi strategy.minimal_roi = min_roi
trade = Trade( trade = Trade(
@ -389,7 +389,7 @@ def test_min_roi_reached3(default_conf, fee) -> None:
def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom, def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom,
profit2, adjusted2, expected2, custom_stop) -> None: profit2, adjusted2, expected2, custom_stop) -> None:
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
trade = Trade( trade = Trade(
@ -402,7 +402,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
exchange='binance', exchange='binance',
open_rate=1, open_rate=1,
) )
trade.adjust_min_max_rates(trade.open_rate) trade.adjust_min_max_rates(trade.open_rate, trade.open_rate)
strategy.trailing_stop = trailing strategy.trailing_stop = trailing
strategy.trailing_stop_positive = -0.05 strategy.trailing_stop_positive = -0.05
strategy.use_custom_stoploss = custom strategy.use_custom_stoploss = custom
@ -437,7 +437,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
def test_custom_sell(default_conf, fee, caplog) -> None: def test_custom_sell(default_conf, fee, caplog) -> None:
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
trade = Trade( trade = Trade(
@ -491,7 +491,7 @@ def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None:
advise_sell=sell_mock, advise_sell=sell_mock,
) )
strategy = DefaultStrategy({}) strategy = StrategyTestV2({})
strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'}) strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'})
assert ind_mock.call_count == 1 assert ind_mock.call_count == 1
assert buy_mock.call_count == 1 assert buy_mock.call_count == 1
@ -522,7 +522,7 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) ->
advise_sell=sell_mock, advise_sell=sell_mock,
) )
strategy = DefaultStrategy({}) strategy = StrategyTestV2({})
strategy.dp = DataProvider({}, None, None) strategy.dp = DataProvider({}, None, None)
strategy.process_only_new_candles = True strategy.process_only_new_candles = True
@ -554,8 +554,9 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) ->
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_is_pair_locked(default_conf): def test_is_pair_locked(default_conf):
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'StrategyTestV2'})
PairLocks.timeframe = default_conf['timeframe'] PairLocks.timeframe = default_conf['timeframe']
PairLocks.use_db = True
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
# No lock should be present # No lock should be present
assert len(PairLocks.get_pair_locks(None)) == 0 assert len(PairLocks.get_pair_locks(None)) == 0
@ -606,7 +607,7 @@ def test_is_pair_locked(default_conf):
def test_is_informative_pairs_callback(default_conf): def test_is_informative_pairs_callback(default_conf):
default_conf.update({'strategy': 'TestStrategyLegacy'}) default_conf.update({'strategy': 'TestStrategyLegacyV1'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
# Should return empty # Should return empty
# Uses fallback to base implementation # Uses fallback to base implementation
@ -633,7 +634,7 @@ def test_strategy_safe_wrapper_error(caplog, error):
assert ret assert ret
caplog.clear() caplog.clear()
# Test supressing error # Test suppressing error
ret = strategy_safe_wrapper(failing_method, message='DeadBeef', supress_error=True)() ret = strategy_safe_wrapper(failing_method, message='DeadBeef', supress_error=True)()
assert log_has_re(r'DeadBeef.*', caplog) assert log_has_re(r'DeadBeef.*', caplog)

View File

@ -18,7 +18,7 @@ def test_search_strategy():
s, _ = StrategyResolver._search_object( s, _ = StrategyResolver._search_object(
directory=default_location, directory=default_location,
object_name='DefaultStrategy', object_name='StrategyTestV2',
add_source=True, add_source=True,
) )
assert issubclass(s, IStrategy) assert issubclass(s, IStrategy)
@ -74,10 +74,10 @@ def test_load_strategy_base64(result, caplog, default_conf):
def test_load_strategy_invalid_directory(result, caplog, default_conf): def test_load_strategy_invalid_directory(result, caplog, default_conf):
default_conf['strategy'] = 'DefaultStrategy' default_conf['strategy'] = 'StrategyTestV2'
extra_dir = Path.cwd() / 'some/path' extra_dir = Path.cwd() / 'some/path'
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
StrategyResolver._load_strategy('DefaultStrategy', config=default_conf, StrategyResolver._load_strategy('StrategyTestV2', config=default_conf,
extra_dir=extra_dir) extra_dir=extra_dir)
assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog) assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog)
@ -100,7 +100,7 @@ def test_load_strategy_noname(default_conf):
def test_strategy(result, default_conf): def test_strategy(result, default_conf):
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
metadata = {'pair': 'ETH/BTC'} metadata = {'pair': 'ETH/BTC'}
@ -127,7 +127,7 @@ def test_strategy(result, default_conf):
def test_strategy_override_minimal_roi(caplog, default_conf): def test_strategy_override_minimal_roi(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'StrategyTestV2',
'minimal_roi': { 'minimal_roi': {
"20": 0.1, "20": 0.1,
"0": 0.5 "0": 0.5
@ -144,7 +144,7 @@ def test_strategy_override_minimal_roi(caplog, default_conf):
def test_strategy_override_stoploss(caplog, default_conf): def test_strategy_override_stoploss(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'StrategyTestV2',
'stoploss': -0.5 'stoploss': -0.5
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
@ -156,7 +156,7 @@ def test_strategy_override_stoploss(caplog, default_conf):
def test_strategy_override_trailing_stop(caplog, default_conf): def test_strategy_override_trailing_stop(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'StrategyTestV2',
'trailing_stop': True 'trailing_stop': True
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
@ -169,7 +169,7 @@ def test_strategy_override_trailing_stop(caplog, default_conf):
def test_strategy_override_trailing_stop_positive(caplog, default_conf): def test_strategy_override_trailing_stop_positive(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'StrategyTestV2',
'trailing_stop_positive': -0.1, 'trailing_stop_positive': -0.1,
'trailing_stop_positive_offset': -0.2 'trailing_stop_positive_offset': -0.2
@ -189,7 +189,7 @@ def test_strategy_override_timeframe(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'StrategyTestV2',
'timeframe': 60, 'timeframe': 60,
'stake_currency': 'ETH' 'stake_currency': 'ETH'
}) })
@ -205,7 +205,7 @@ def test_strategy_override_process_only_new_candles(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'StrategyTestV2',
'process_only_new_candles': True 'process_only_new_candles': True
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
@ -225,7 +225,7 @@ def test_strategy_override_order_types(caplog, default_conf):
'stoploss_on_exchange': True, 'stoploss_on_exchange': True,
} }
default_conf.update({ default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'StrategyTestV2',
'order_types': order_types 'order_types': order_types
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
@ -239,12 +239,12 @@ def test_strategy_override_order_types(caplog, default_conf):
" 'stoploss_on_exchange': True}.", caplog) " 'stoploss_on_exchange': True}.", caplog)
default_conf.update({ default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'StrategyTestV2',
'order_types': {'buy': 'market'} 'order_types': {'buy': 'market'}
}) })
# Raise error for invalid configuration # Raise error for invalid configuration
with pytest.raises(ImportError, with pytest.raises(ImportError,
match=r"Impossible to load Strategy 'DefaultStrategy'. " match=r"Impossible to load Strategy 'StrategyTestV2'. "
r"Order-types mapping is incomplete."): r"Order-types mapping is incomplete."):
StrategyResolver.load_strategy(default_conf) StrategyResolver.load_strategy(default_conf)
@ -258,7 +258,7 @@ def test_strategy_override_order_tif(caplog, default_conf):
} }
default_conf.update({ default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'StrategyTestV2',
'order_time_in_force': order_time_in_force 'order_time_in_force': order_time_in_force
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
@ -271,12 +271,12 @@ def test_strategy_override_order_tif(caplog, default_conf):
" {'buy': 'fok', 'sell': 'gtc'}.", caplog) " {'buy': 'fok', 'sell': 'gtc'}.", caplog)
default_conf.update({ default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'StrategyTestV2',
'order_time_in_force': {'buy': 'fok'} 'order_time_in_force': {'buy': 'fok'}
}) })
# Raise error for invalid configuration # Raise error for invalid configuration
with pytest.raises(ImportError, with pytest.raises(ImportError,
match=r"Impossible to load Strategy 'DefaultStrategy'. " match=r"Impossible to load Strategy 'StrategyTestV2'. "
r"Order-time-in-force mapping is incomplete."): r"Order-time-in-force mapping is incomplete."):
StrategyResolver.load_strategy(default_conf) StrategyResolver.load_strategy(default_conf)
@ -284,7 +284,7 @@ def test_strategy_override_order_tif(caplog, default_conf):
def test_strategy_override_use_sell_signal(caplog, default_conf): def test_strategy_override_use_sell_signal(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'StrategyTestV2',
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert strategy.use_sell_signal assert strategy.use_sell_signal
@ -294,7 +294,7 @@ def test_strategy_override_use_sell_signal(caplog, default_conf):
assert default_conf['use_sell_signal'] assert default_conf['use_sell_signal']
default_conf.update({ default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'StrategyTestV2',
'use_sell_signal': False, 'use_sell_signal': False,
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
@ -307,7 +307,7 @@ def test_strategy_override_use_sell_signal(caplog, default_conf):
def test_strategy_override_use_sell_profit_only(caplog, default_conf): def test_strategy_override_use_sell_profit_only(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'StrategyTestV2',
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert not strategy.sell_profit_only assert not strategy.sell_profit_only
@ -317,7 +317,7 @@ def test_strategy_override_use_sell_profit_only(caplog, default_conf):
assert not default_conf['sell_profit_only'] assert not default_conf['sell_profit_only']
default_conf.update({ default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'StrategyTestV2',
'sell_profit_only': True, 'sell_profit_only': True,
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
@ -330,7 +330,7 @@ def test_strategy_override_use_sell_profit_only(caplog, default_conf):
@pytest.mark.filterwarnings("ignore:deprecated") @pytest.mark.filterwarnings("ignore:deprecated")
def test_deprecate_populate_indicators(result, default_conf): def test_deprecate_populate_indicators(result, default_conf):
default_location = Path(__file__).parent / "strats" default_location = Path(__file__).parent / "strats"
default_conf.update({'strategy': 'TestStrategyLegacy', default_conf.update({'strategy': 'TestStrategyLegacyV1',
'strategy_path': default_location}) 'strategy_path': default_location})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
with warnings.catch_warnings(record=True) as w: with warnings.catch_warnings(record=True) as w:
@ -365,7 +365,7 @@ def test_deprecate_populate_indicators(result, default_conf):
def test_call_deprecated_function(result, monkeypatch, default_conf, caplog): def test_call_deprecated_function(result, monkeypatch, default_conf, caplog):
default_location = Path(__file__).parent / "strats" default_location = Path(__file__).parent / "strats"
del default_conf['timeframe'] del default_conf['timeframe']
default_conf.update({'strategy': 'TestStrategyLegacy', default_conf.update({'strategy': 'TestStrategyLegacyV1',
'strategy_path': default_location}) 'strategy_path': default_location})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
metadata = {'pair': 'ETH/BTC'} metadata = {'pair': 'ETH/BTC'}
@ -395,7 +395,7 @@ def test_call_deprecated_function(result, monkeypatch, default_conf, caplog):
def test_strategy_interface_versioning(result, monkeypatch, default_conf): def test_strategy_interface_versioning(result, monkeypatch, default_conf):
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
metadata = {'pair': 'ETH/BTC'} metadata = {'pair': 'ETH/BTC'}

View File

@ -123,7 +123,7 @@ def test_parse_args_backtesting_custom() -> None:
'-c', 'test_conf.json', '-c', 'test_conf.json',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--strategy-list', '--strategy-list',
'DefaultStrategy', 'StrategyTestV2',
'SampleStrategy' 'SampleStrategy'
] ]
call_args = Arguments(args).get_parsed_arg() call_args = Arguments(args).get_parsed_arg()

View File

@ -404,7 +404,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
arglist = [ arglist = [
'backtesting', 'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'StrategyTestV2',
] ]
args = Arguments(arglist).get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
@ -441,7 +441,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
arglist = [ arglist = [
'backtesting', 'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'StrategyTestV2',
'--datadir', '/foo/bar', '--datadir', '/foo/bar',
'--userdir', "/tmp/freqtrade", '--userdir', "/tmp/freqtrade",
'--ticker-interval', '1m', '--ticker-interval', '1m',
@ -498,7 +498,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--export', 'trades', '--export', 'trades',
'--strategy-list', '--strategy-list',
'DefaultStrategy', 'StrategyTestV2',
'TestStrategy' 'TestStrategy'
] ]

View File

@ -185,7 +185,7 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b
limit_buy_order_open['id'] = str(i) limit_buy_order_open['id'] = str(i)
result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC') result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC')
assert pytest.approx(result) == expected[i] assert pytest.approx(result) == expected[i]
freqtrade.execute_buy('ETH/BTC', result) freqtrade.execute_entry('ETH/BTC', result)
else: else:
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
freqtrade.wallets.get_trade_stake_amount('ETH/BTC') freqtrade.wallets.get_trade_stake_amount('ETH/BTC')
@ -584,8 +584,8 @@ def test_create_trades_preopen(default_conf, ticker, fee, mocker, limit_buy_orde
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
# Create 2 existing trades # Create 2 existing trades
freqtrade.execute_buy('ETH/BTC', default_conf['stake_amount']) freqtrade.execute_entry('ETH/BTC', default_conf['stake_amount'])
freqtrade.execute_buy('NEO/BTC', default_conf['stake_amount']) freqtrade.execute_entry('NEO/BTC', default_conf['stake_amount'])
assert len(Trade.get_open_trades()) == 2 assert len(Trade.get_open_trades()) == 2
# Change order_id for new orders # Change order_id for new orders
@ -776,7 +776,7 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None:
assert ("ETH/BTC", default_conf["timeframe"]) in refresh_mock.call_args[0][0] assert ("ETH/BTC", default_conf["timeframe"]) in refresh_mock.call_args[0][0]
def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order_open) -> None: def test_execute_entry(mocker, default_conf, fee, limit_buy_order, limit_buy_order_open) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
@ -799,7 +799,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
) )
pair = 'ETH/BTC' pair = 'ETH/BTC'
assert not freqtrade.execute_buy(pair, stake_amount) assert not freqtrade.execute_entry(pair, stake_amount)
assert buy_rate_mock.call_count == 1 assert buy_rate_mock.call_count == 1
assert buy_mm.call_count == 0 assert buy_mm.call_count == 0
assert freqtrade.strategy.confirm_trade_entry.call_count == 1 assert freqtrade.strategy.confirm_trade_entry.call_count == 1
@ -807,7 +807,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
limit_buy_order_open['id'] = '22' limit_buy_order_open['id'] = '22'
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
assert freqtrade.execute_buy(pair, stake_amount) assert freqtrade.execute_entry(pair, stake_amount)
assert buy_rate_mock.call_count == 1 assert buy_rate_mock.call_count == 1
assert buy_mm.call_count == 1 assert buy_mm.call_count == 1
call_args = buy_mm.call_args_list[0][1] call_args = buy_mm.call_args_list[0][1]
@ -826,7 +826,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
# Test calling with price # Test calling with price
limit_buy_order_open['id'] = '33' limit_buy_order_open['id'] = '33'
fix_price = 0.06 fix_price = 0.06
assert freqtrade.execute_buy(pair, stake_amount, fix_price) assert freqtrade.execute_entry(pair, stake_amount, fix_price)
# Make sure get_rate wasn't called again # Make sure get_rate wasn't called again
assert buy_rate_mock.call_count == 0 assert buy_rate_mock.call_count == 0
@ -844,7 +844,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
mocker.patch('freqtrade.exchange.Exchange.create_order', mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=limit_buy_order)) MagicMock(return_value=limit_buy_order))
assert freqtrade.execute_buy(pair, stake_amount) assert freqtrade.execute_entry(pair, stake_amount)
trade = Trade.query.all()[2] trade = Trade.query.all()[2]
assert trade assert trade
assert trade.open_order_id is None assert trade.open_order_id is None
@ -861,7 +861,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
limit_buy_order['id'] = '555' limit_buy_order['id'] = '555'
mocker.patch('freqtrade.exchange.Exchange.create_order', mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=limit_buy_order)) MagicMock(return_value=limit_buy_order))
assert freqtrade.execute_buy(pair, stake_amount) assert freqtrade.execute_entry(pair, stake_amount)
trade = Trade.query.all()[3] trade = Trade.query.all()[3]
assert trade assert trade
assert trade.open_order_id == '555' assert trade.open_order_id == '555'
@ -873,7 +873,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
limit_buy_order['id'] = '556' limit_buy_order['id'] = '556'
freqtrade.strategy.custom_stake_amount = lambda **kwargs: 150.0 freqtrade.strategy.custom_stake_amount = lambda **kwargs: 150.0
assert freqtrade.execute_buy(pair, stake_amount) assert freqtrade.execute_entry(pair, stake_amount)
trade = Trade.query.all()[4] trade = Trade.query.all()[4]
assert trade assert trade
assert trade.stake_amount == 150 assert trade.stake_amount == 150
@ -881,7 +881,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
# Exception case # Exception case
limit_buy_order['id'] = '557' limit_buy_order['id'] = '557'
freqtrade.strategy.custom_stake_amount = lambda **kwargs: 20 / 0 freqtrade.strategy.custom_stake_amount = lambda **kwargs: 20 / 0
assert freqtrade.execute_buy(pair, stake_amount) assert freqtrade.execute_entry(pair, stake_amount)
trade = Trade.query.all()[5] trade = Trade.query.all()[5]
assert trade assert trade
assert trade.stake_amount == 2.0 assert trade.stake_amount == 2.0
@ -896,16 +896,50 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
limit_buy_order['id'] = '66' limit_buy_order['id'] = '66'
mocker.patch('freqtrade.exchange.Exchange.create_order', mocker.patch('freqtrade.exchange.Exchange.create_order',
MagicMock(return_value=limit_buy_order)) MagicMock(return_value=limit_buy_order))
assert not freqtrade.execute_buy(pair, stake_amount) assert not freqtrade.execute_entry(pair, stake_amount)
# Fail to get price... # Fail to get price...
mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(return_value=0.0)) mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(return_value=0.0))
with pytest.raises(PricingError, match="Could not determine buy price."): with pytest.raises(PricingError, match="Could not determine buy price."):
freqtrade.execute_buy(pair, stake_amount) freqtrade.execute_entry(pair, stake_amount)
# In case of custom entry price
mocker.patch('freqtrade.exchange.Exchange.get_rate', return_value=0.50)
limit_buy_order['status'] = 'open'
limit_buy_order['id'] = '5566'
freqtrade.strategy.custom_entry_price = lambda **kwargs: 0.508
assert freqtrade.execute_entry(pair, stake_amount)
trade = Trade.query.all()[6]
assert trade
assert trade.open_rate_requested == 0.508
# In case of custom entry price set to None
limit_buy_order['status'] = 'open'
limit_buy_order['id'] = '5567'
freqtrade.strategy.custom_entry_price = lambda **kwargs: None
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_rate=MagicMock(return_value=10),
)
assert freqtrade.execute_entry(pair, stake_amount)
trade = Trade.query.all()[7]
assert trade
assert trade.open_rate_requested == 10
# In case of custom entry price not float type
limit_buy_order['status'] = 'open'
limit_buy_order['id'] = '5568'
freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price"
assert freqtrade.execute_entry(pair, stake_amount)
trade = Trade.query.all()[8]
assert trade
assert trade.open_rate_requested == 10
def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None: def test_execute_entry_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
@ -923,18 +957,18 @@ def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -
pair = 'ETH/BTC' pair = 'ETH/BTC'
freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=ValueError) freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=ValueError)
assert freqtrade.execute_buy(pair, stake_amount) assert freqtrade.execute_entry(pair, stake_amount)
limit_buy_order['id'] = '222' limit_buy_order['id'] = '222'
freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=Exception) freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=Exception)
assert freqtrade.execute_buy(pair, stake_amount) assert freqtrade.execute_entry(pair, stake_amount)
limit_buy_order['id'] = '2223' limit_buy_order['id'] = '2223'
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
assert freqtrade.execute_buy(pair, stake_amount) assert freqtrade.execute_entry(pair, stake_amount)
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False)
assert not freqtrade.execute_buy(pair, stake_amount) assert not freqtrade.execute_entry(pair, stake_amount)
def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None: def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None:
@ -1929,7 +1963,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open,
assert nb_trades == 0 assert nb_trades == 0
# Buy is triggering, so buying ... # Buy is triggering, so buying ...
patch_get_signal(freqtrade, value=(True, False, None)) patch_get_signal(freqtrade)
freqtrade.enter_positions() freqtrade.enter_positions()
trades = Trade.query.all() trades = Trade.query.all()
nb_trades = len(trades) nb_trades = len(trades)
@ -1974,7 +2008,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open,
) )
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtrade, value=(True, False, None)) patch_get_signal(freqtrade)
freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True)
freqtrade.enter_positions() freqtrade.enter_positions()
@ -1982,7 +2016,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open,
trade = Trade.query.first() trade = Trade.query.first()
trade.is_open = True trade.is_open = True
# FIX: sniffing logs, suggest handle_trade should not execute_sell # FIX: sniffing logs, suggest handle_trade should not execute_trade_exit
# instead that responsibility should be moved out of handle_trade(), # instead that responsibility should be moved out of handle_trade(),
# we might just want to check if we are in a sell condition without # we might just want to check if we are in a sell condition without
# executing # executing
@ -2609,7 +2643,7 @@ def test_handle_cancel_sell_cancel_exception(mocker, default_conf) -> None:
assert freqtrade.handle_cancel_sell(trade, order, reason) == 'error cancelling order' assert freqtrade.handle_cancel_sell(trade, order, reason) == 'error cancelling order'
def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None: def test_execute_trade_exit_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
@ -2637,16 +2671,16 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
fetch_ticker=ticker_sell_up fetch_ticker=ticker_sell_up
) )
# Prevented sell ... # Prevented sell ...
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_up()['bid'],
sell_reason=SellCheckTuple(sell_type=SellType.ROI)) sell_reason=SellCheckTuple(sell_type=SellType.ROI))
assert rpc_mock.call_count == 0 assert rpc_mock.call_count == 0
assert freqtrade.strategy.confirm_trade_exit.call_count == 1 assert freqtrade.strategy.confirm_trade_exit.call_count == 1
# Repatch with true # Repatch with true
freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True)
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_up()['bid'],
sell_reason=SellCheckTuple(sell_type=SellType.ROI)) sell_reason=SellCheckTuple(sell_type=SellType.ROI))
assert freqtrade.strategy.confirm_trade_exit.call_count == 1 assert freqtrade.strategy.confirm_trade_exit.call_count == 1
assert rpc_mock.call_count == 1 assert rpc_mock.call_count == 1
@ -2673,7 +2707,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
} == last_msg } == last_msg
def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) -> None: def test_execute_trade_exit_down(default_conf, ticker, fee, ticker_sell_down, mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
@ -2698,8 +2732,8 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
fetch_ticker=ticker_sell_down fetch_ticker=ticker_sell_down
) )
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_down()['bid'],
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2
last_msg = rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
@ -2725,8 +2759,73 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
} == last_msg } == last_msg
def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fee, def test_execute_trade_exit_custom_exit_price(default_conf, ticker, fee, ticker_sell_up,
ticker_sell_down, mocker) -> None: mocker) -> None:
rpc_mock = patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
_is_dry_limit_order_filled=MagicMock(return_value=False),
)
patch_whitelist(mocker, default_conf)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=False)
# Create some test data
freqtrade.enter_positions()
rpc_mock.reset_mock()
trade = Trade.query.first()
assert trade
assert freqtrade.strategy.confirm_trade_exit.call_count == 0
# Increase the price and sell it
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker_sell_up
)
freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True)
# Set a custom exit price
freqtrade.strategy.custom_exit_price = lambda **kwargs: 1.170e-05
freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_up()['bid'],
sell_reason=SellCheckTuple(sell_type=SellType.SELL_SIGNAL))
# Sell price must be different to default bid price
assert freqtrade.strategy.confirm_trade_exit.call_count == 1
assert rpc_mock.call_count == 1
last_msg = rpc_mock.call_args_list[-1][0][0]
assert {
'trade_id': 1,
'type': RPCMessageType.SELL,
'exchange': 'Binance',
'pair': 'ETH/BTC',
'gain': 'profit',
'limit': 1.170e-05,
'amount': 91.07468123,
'order_type': 'limit',
'open_rate': 1.098e-05,
'current_rate': 1.173e-05,
'profit_amount': 6.041e-05,
'profit_ratio': 0.06025919,
'stake_currency': 'BTC',
'fiat_currency': 'USD',
'sell_reason': SellType.SELL_SIGNAL.value,
'open_date': ANY,
'close_date': ANY,
'close_rate': ANY,
} == last_msg
def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(default_conf, ticker, fee,
ticker_sell_down, mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
@ -2756,8 +2855,8 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
# Setting trade stoploss to 0.01 # Setting trade stoploss to 0.01
trade.stop_loss = 0.00001099 * 0.99 trade.stop_loss = 0.00001099 * 0.99
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_down()['bid'],
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2
last_msg = rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
@ -2784,7 +2883,8 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
} == last_msg } == last_msg
def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, caplog) -> None: def test_execute_trade_exit_sloe_cancel_exception(
mocker, default_conf, ticker, fee, caplog) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
side_effect=InvalidOrderException()) side_effect=InvalidOrderException())
@ -2811,14 +2911,14 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c
freqtrade.config['dry_run'] = False freqtrade.config['dry_run'] = False
trade.stoploss_order_id = "abcd" trade.stoploss_order_id = "abcd"
freqtrade.execute_sell(trade=trade, limit=1234, freqtrade.execute_trade_exit(trade=trade, limit=1234,
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
assert create_order_mock.call_count == 2 assert create_order_mock.call_count == 2
assert log_has('Could not cancel stoploss order abcd', caplog) assert log_has('Could not cancel stoploss order abcd', caplog)
def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticker_sell_up, def test_execute_trade_exit_with_stoploss_on_exchange(default_conf, ticker, fee, ticker_sell_up,
mocker) -> None: mocker) -> None:
default_conf['exchange']['name'] = 'binance' default_conf['exchange']['name'] = 'binance'
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
@ -2862,8 +2962,8 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
fetch_ticker=ticker_sell_up fetch_ticker=ticker_sell_up
) )
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_up()['bid'],
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -2871,8 +2971,8 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
assert rpc_mock.call_count == 3 assert rpc_mock.call_count == 3
def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, fee, def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(default_conf, ticker, fee,
mocker) -> None: mocker) -> None:
default_conf['exchange']['name'] = 'binance' default_conf['exchange']['name'] = 'binance'
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
@ -2943,8 +3043,8 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL
def test_execute_sell_market_order(default_conf, ticker, fee, def test_execute_trade_exit_market_order(default_conf, ticker, fee,
ticker_sell_up, mocker) -> None: ticker_sell_up, mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
@ -2970,8 +3070,8 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
) )
freqtrade.config['order_types']['sell'] = 'market' freqtrade.config['order_types']['sell'] = 'market'
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_up()['bid'],
sell_reason=SellCheckTuple(sell_type=SellType.ROI)) sell_reason=SellCheckTuple(sell_type=SellType.ROI))
assert not trade.is_open assert not trade.is_open
assert trade.close_profit == 0.0620716 assert trade.close_profit == 0.0620716
@ -3001,8 +3101,8 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
} == last_msg } == last_msg
def test_execute_sell_insufficient_funds_error(default_conf, ticker, fee, def test_execute_trade_exit_insufficient_funds_error(default_conf, ticker, fee,
ticker_sell_up, mocker) -> None: ticker_sell_up, mocker) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds')
mocker.patch.multiple( mocker.patch.multiple(
@ -3029,8 +3129,8 @@ def test_execute_sell_insufficient_funds_error(default_conf, ticker, fee,
) )
sell_reason = SellCheckTuple(sell_type=SellType.ROI) sell_reason = SellCheckTuple(sell_type=SellType.ROI)
assert not freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], assert not freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_up()['bid'],
sell_reason=sell_reason) sell_reason=sell_reason)
assert mock_insuf.call_count == 1 assert mock_insuf.call_count == 1
@ -3286,8 +3386,8 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo
fetch_ticker=ticker_sell_down fetch_ticker=ticker_sell_down
) )
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_down()['bid'],
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
trade.close(ticker_sell_down()['bid']) trade.close(ticker_sell_down()['bid'])
assert freqtrade.strategy.is_pair_locked(trade.pair) assert freqtrade.strategy.is_pair_locked(trade.pair)
@ -4512,3 +4612,43 @@ def test_refind_lost_order(mocker, default_conf, fee, caplog):
freqtrade.refind_lost_order(trades[4]) freqtrade.refind_lost_order(trades[4])
assert log_has(f"Error updating {order['id']}.", caplog) assert log_has(f"Error updating {order['id']}.", caplog)
def test_get_valid_price(mocker, default_conf) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
freqtrade = FreqtradeBot(default_conf)
freqtrade.config['custom_price_max_distance_ratio'] = 0.02
custom_price_string = "10"
custom_price_badstring = "10abc"
custom_price_float = 10.0
custom_price_int = 10
custom_price_over_max_alwd = 11.0
custom_price_under_min_alwd = 9.0
proposed_price = 10.1
valid_price_from_string = freqtrade.get_valid_price(custom_price_string, proposed_price)
valid_price_from_badstring = freqtrade.get_valid_price(custom_price_badstring, proposed_price)
valid_price_from_int = freqtrade.get_valid_price(custom_price_int, proposed_price)
valid_price_from_float = freqtrade.get_valid_price(custom_price_float, proposed_price)
valid_price_at_max_alwd = freqtrade.get_valid_price(custom_price_over_max_alwd, proposed_price)
valid_price_at_min_alwd = freqtrade.get_valid_price(custom_price_under_min_alwd, proposed_price)
assert isinstance(valid_price_from_string, float)
assert isinstance(valid_price_from_badstring, float)
assert isinstance(valid_price_from_int, float)
assert isinstance(valid_price_from_float, float)
assert valid_price_from_string == custom_price_float
assert valid_price_from_badstring == proposed_price
assert valid_price_from_int == custom_price_int
assert valid_price_from_float == custom_price_float
assert valid_price_at_max_alwd < custom_price_over_max_alwd
assert valid_price_at_max_alwd > proposed_price
assert valid_price_at_min_alwd > custom_price_under_min_alwd
assert valid_price_at_min_alwd < proposed_price

View File

@ -9,7 +9,7 @@ from freqtrade.strategy.interface import SellCheckTuple
from tests.conftest import get_patched_freqtradebot, patch_get_signal from tests.conftest import get_patched_freqtradebot, patch_get_signal
def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
limit_buy_order, mocker) -> None: limit_buy_order, mocker) -> None:
""" """
Tests workflow of selling stoploss_on_exchange. Tests workflow of selling stoploss_on_exchange.

View File

@ -1587,25 +1587,30 @@ def test_adjust_min_max_rates(fee):
open_rate=1, open_rate=1,
) )
trade.adjust_min_max_rates(trade.open_rate) trade.adjust_min_max_rates(trade.open_rate, trade.open_rate)
assert trade.max_rate == 1 assert trade.max_rate == 1
assert trade.min_rate == 1 assert trade.min_rate == 1
# check min adjusted, max remained # check min adjusted, max remained
trade.adjust_min_max_rates(0.96) trade.adjust_min_max_rates(0.96, 0.96)
assert trade.max_rate == 1 assert trade.max_rate == 1
assert trade.min_rate == 0.96 assert trade.min_rate == 0.96
# check max adjusted, min remains # check max adjusted, min remains
trade.adjust_min_max_rates(1.05) trade.adjust_min_max_rates(1.05, 1.05)
assert trade.max_rate == 1.05 assert trade.max_rate == 1.05
assert trade.min_rate == 0.96 assert trade.min_rate == 0.96
# current rate "in the middle" - no adjustment # current rate "in the middle" - no adjustment
trade.adjust_min_max_rates(1.03) trade.adjust_min_max_rates(1.03, 1.03)
assert trade.max_rate == 1.05 assert trade.max_rate == 1.05
assert trade.min_rate == 0.96 assert trade.min_rate == 0.96
# current rate "in the middle" - no adjustment
trade.adjust_min_max_rates(1.10, 0.91)
assert trade.max_rate == 1.10
assert trade.min_rate == 0.91
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize('use_db', [True, False]) @pytest.mark.parametrize('use_db', [True, False])
@ -2099,6 +2104,11 @@ def test_update_order_from_ccxt(caplog):
assert o.ft_is_open assert o.ft_is_open
assert o.order_filled_date is None assert o.order_filled_date is None
# Order is unfilled, "filled" not set
# https://github.com/freqtrade/freqtrade/issues/5404
ccxt_order.update({'filled': None, 'remaining': 20.0, 'status': 'canceled'})
o.update_from_ccxt_object(ccxt_order)
# Order has been closed # Order has been closed
ccxt_order.update({'filled': 20.0, 'remaining': 0.0, 'status': 'closed'}) ccxt_order.update({'filled': 20.0, 'remaining': 0.0, 'status': 'closed'})
o.update_from_ccxt_object(ccxt_order) o.update_from_ccxt_object(ccxt_order)

View File

@ -70,7 +70,6 @@ def test_add_indicators(default_conf, testdatadir, caplog):
indicators1 = {"ema10": {}} indicators1 = {"ema10": {}}
indicators2 = {"macd": {"color": "red"}} indicators2 = {"macd": {"color": "red"}}
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
# Generate buy/sell signals and indicators # Generate buy/sell signals and indicators
@ -112,7 +111,6 @@ def test_add_areas(default_conf, testdatadir, caplog):
"fill_to": "macdhist"}} "fill_to": "macdhist"}}
ind_plain = {"macd": {"fill_to": "macdhist"}} ind_plain = {"macd": {"fill_to": "macdhist"}}
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
# Generate buy/sell signals and indicators # Generate buy/sell signals and indicators
@ -239,7 +237,6 @@ def test_generate_candlestick_graph_no_trades(default_conf, mocker, testdatadir)
data = history.load_pair_history(pair=pair, timeframe='1m', data = history.load_pair_history(pair=pair, timeframe='1m',
datadir=testdatadir, timerange=timerange) datadir=testdatadir, timerange=timerange)
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
# Generate buy/sell signals and indicators # Generate buy/sell signals and indicators

View File

@ -157,13 +157,13 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r
assert result == result1 assert result == result1
# create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)'
freqtrade.execute_buy('ETH/USDT', result) freqtrade.execute_entry('ETH/USDT', result)
result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT') result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT')
assert result == result1 assert result == result1
# create 2 trades, order amount should be None # create 2 trades, order amount should be None
freqtrade.execute_buy('LTC/BTC', result) freqtrade.execute_entry('LTC/BTC', result)
result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT') result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT')
assert result == 0 assert result == 0

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long