Merge pull request #4609 from freqtrade/new_release

New release 2021.3
This commit is contained in:
Matthias 2021-03-28 11:32:33 +02:00 committed by GitHub
commit 2cf36ca545
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
95 changed files with 2255 additions and 1110 deletions

View File

@ -102,7 +102,7 @@ jobs:
mypy freqtrade scripts
- name: Slack Notification
uses: homoluctus/slatify@v1.8.0
uses: lazy-actions/slatify@v3.0.0
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with:
type: ${{ job.status }}
@ -194,7 +194,7 @@ jobs:
mypy freqtrade scripts
- name: Slack Notification
uses: homoluctus/slatify@v1.8.0
uses: lazy-actions/slatify@v3.0.0
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with:
type: ${{ job.status }}
@ -257,7 +257,7 @@ jobs:
mypy freqtrade scripts
- name: Slack Notification
uses: homoluctus/slatify@v1.8.0
uses: lazy-actions/slatify@v3.0.0
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with:
type: ${{ job.status }}
@ -288,7 +288,7 @@ jobs:
mkdocs build
- name: Slack Notification
uses: homoluctus/slatify@v1.8.0
uses: lazy-actions/slatify@v3.0.0
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with:
type: ${{ job.status }}
@ -311,7 +311,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Slack Notification
uses: homoluctus/slatify@v1.8.0
uses: lazy-actions/slatify@v3.0.0
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with:
type: ${{ job.status }}
@ -398,7 +398,7 @@ jobs:
- name: Slack Notification
uses: homoluctus/slatify@v1.8.0
uses: lazy-actions/slatify@v3.0.0
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with:
type: ${{ job.status }}

View File

@ -41,7 +41,9 @@ COPY --from=python-deps /root/.local /root/.local
# Install and execute
COPY . /freqtrade/
RUN pip install -e . --no-cache-dir \
RUN apt-get install -y libhdf5-serial-dev \
&& apt-get clean \
&& pip install -e . --no-cache-dir \
&& freqtrade install-ui
ENTRYPOINT ["freqtrade"]

View File

@ -41,13 +41,13 @@
"ETH/BTC",
"LTC/BTC",
"ETC/BTC",
"DASH/BTC",
"ZEC/BTC",
"RVN/BTC",
"CRO/BTC",
"XLM/BTC",
"XRP/BTC",
"TRX/BTC",
"ADA/BTC",
"XMR/BTC"
"DOT/BTC"
],
"pair_blacklist": [
"DOGE/BTC"

View File

@ -49,6 +49,8 @@
"buy": "limit",
"sell": "limit",
"emergencysell": "market",
"forcesell": "market",
"forcebuy": "market",
"stoploss": "market",
"stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60
@ -111,7 +113,7 @@
"password": "",
"ccxt_config": {"enableRateLimit": true},
"ccxt_async_config": {
"enableRateLimit": false,
"enableRateLimit": true,
"rateLimit": 500,
"aiohttp_trust_env": false
},

View File

@ -6,7 +6,7 @@ class.
## Derived hyperopt classes
Custom hyperop classes can be derived in the same way [it can be done for strategies](strategy-customization.md#derived-strategies).
Custom hyperopt classes can be derived in the same way [it can be done for strategies](strategy-customization.md#derived-strategies).
Applying to hyperoptimization, as an example, you may override how dimensions are defined in your optimization hyperspace:
@ -32,6 +32,51 @@ or
$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ...
```
## Sharing methods with your strategy
Hyperopt classes provide access to the Strategy via the `strategy` class attribute.
This can be a great way to reduce code duplication if used correctly, but will also complicate usage for inexperienced users.
``` python
from pandas import DataFrame
from freqtrade.strategy.interface import IStrategy
import freqtrade.vendor.qtpylib.indicators as qtpylib
class MyAwesomeStrategy(IStrategy):
buy_params = {
'rsi-value': 30,
'adx-value': 35,
}
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return self.buy_strategy_generator(self.buy_params, dataframe, metadata)
@staticmethod
def buy_strategy_generator(params, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
qtpylib.crossed_above(dataframe['rsi'], params['rsi-value']) &
dataframe['adx'] > params['adx-value']) &
dataframe['volume'] > 0
)
, 'buy'] = 1
return dataframe
class MyAwesomeHyperOpt(IHyperOpt):
...
@staticmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Define the buy strategy parameters to be used by Hyperopt.
"""
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
# Call strategy's buy strategy generator
return self.StrategyClass.buy_strategy_generator(params, dataframe, metadata)
return populate_buy_trend
```
## Creating and using a custom loss function
To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class.

View File

@ -16,6 +16,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[--max-open-trades INT]
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
[--eps] [--dmmp] [--enable-protections]
[--dry-run-wallet DRY_RUN_WALLET]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
[--export EXPORT] [--export-filename PATH]
@ -48,6 +49,9 @@ optional arguments:
Enable protections for backtesting.Will slow
backtesting down by a considerable amount, but will
include configured protections
--dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET
Starting balance, used for backtesting / hyperopt and
dry-runs.
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
Provide a space-separated list of strategies to
backtest. Please note that ticker-interval needs to be
@ -91,8 +95,7 @@ Strategy arguments:
## Test your strategy with Backtesting
Now you have good Buy and Sell strategies and some historic data, you want to test it against
real data. This is what we call
[backtesting](https://en.wikipedia.org/wiki/Backtesting).
real data. This is what we call [backtesting](https://en.wikipedia.org/wiki/Backtesting).
Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHCLV) data from `user_data/data/<exchange>` by default.
If no data is available for the exchange / pair / timeframe combination, backtesting will ask you to download them first using `freqtrade download-data`.
@ -100,6 +103,8 @@ For details on downloading, please refer to the [Data Downloading](data-download
The result of backtesting will confirm if your bot has better odds of making a profit than a loss.
All profit calculations include fees, and freqtrade will use the exchange's default fees for the calculation.
!!! Warning "Using dynamic pairlists for backtesting"
Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist.
Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed.
@ -107,38 +112,56 @@ The result of backtesting will confirm if your bot has better odds of making a p
To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist.
### Run a backtesting against the currencies listed in your config file
### Starting balance
#### With 5 min candle (OHLCV) data (per default)
Backtesting will require a starting balance, which can be provided as `--dry-run-wallet <balance>` or `--starting-balance <balance>` command line argument, or via `dry_run_wallet` configuration setting.
This amount must be higher than `stake_amount`, otherwise the bot will not be able to simulate any trade.
### Dynamic stake amount
Backtesting supports [dynamic stake amount](configuration.md#dynamic-stake-amount) by configuring `stake_amount` as `"unlimited"`, which will split the starting balance into `max_open_trades` pieces.
Profits from early trades will result in subsequent higher stake amounts, resulting in compounding of profits over the backtesting period.
### Example backtesting commands
With 5 min candle (OHLCV) data (per default)
```bash
freqtrade backtesting
freqtrade backtesting --strategy AwesomeStrategy
```
#### With 1 min candle (OHLCV) data
Where `--strategy AwesomeStrategy` / `-s AwesomeStrategy` refers to the class name of the strategy, which is within a python file in the `user_data/strategies` directory.
---
With 1 min candle (OHLCV) data
```bash
freqtrade backtesting --timeframe 1m
freqtrade backtesting --strategy AwesomeStrategy --timeframe 1m
```
#### Using a different on-disk historical candle (OHLCV) data source
---
Providing a custom starting balance of 1000 (in stake currency)
```bash
freqtrade backtesting --strategy AwesomeStrategy --dry-run-wallet 1000
```
---
Using a different on-disk historical candle (OHLCV) data source
Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory.
You can then use this data for backtesting as follows:
```bash
freqtrade --datadir user_data/data/bittrex-20180101 backtesting
freqtrade backtesting --strategy AwesomeStrategy --datadir user_data/data/bittrex-20180101
```
#### With a (custom) strategy file
---
```bash
freqtrade backtesting -s SampleStrategy
```
Where `-s SampleStrategy` refers to the class name within the strategy file `sample_strategy.py` found in the `freqtrade/user_data/strategies` directory.
#### Comparing multiple Strategies
Comparing multiple Strategies
```bash
freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --timeframe 5m
@ -146,23 +169,29 @@ freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --timefram
Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies.
#### Exporting trades to file
---
Exporting trades to file
```bash
freqtrade backtesting --export trades --config config.json --strategy SampleStrategy
freqtrade backtesting --strategy backtesting --export trades --config config.json
```
The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory.
#### Exporting trades to file specifying a custom filename
---
Exporting trades to file specifying a custom filename
```bash
freqtrade backtesting --export trades --export-filename=backtest_samplestrategy.json
freqtrade backtesting --strategy backtesting --export trades --export-filename=backtest_samplestrategy.json
```
Please also read about the [strategy startup period](strategy-customization.md#strategy-startup-period).
#### Supplying custom fee value
---
Supplying custom fee value
Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt.
To account for this in backtesting, you can use the `--fee` command line option to supply this value to backtesting.
@ -177,26 +206,26 @@ freqtrade backtesting --fee 0.001
!!! Note
Only supply this option (or the corresponding configuration parameter) if you want to experiment with different fee values. By default, Backtesting fetches the default fee from the exchange pair/market info.
#### Running backtest with smaller testset by using timerange
---
Use the `--timerange` argument to change how much of the testset you want to use.
Running backtest with smaller test-set by using timerange
Use the `--timerange` argument to change how much of the test-set you want to use.
For example, running backtesting with the `--timerange=20190501-` option will use all available data starting with May 1st, 2019 from your inputdata.
For example, running backtesting with the `--timerange=20190501-` option will use all available data starting with May 1st, 2019 from your input data.
```bash
freqtrade backtesting --timerange=20190501-
```
You can also specify particular dates or a range span indexed by start and stop.
You can also specify particular date ranges.
The full timerange specification:
- Use tickframes till 2018/01/31: `--timerange=-20180131`
- Use tickframes since 2018/01/31: `--timerange=20180131-`
- Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
- Use tickframes between POSIX timestamps 1527595200 1527618600:
`--timerange=1527595200-1527618600`
- Use data until 2018/01/31: `--timerange=-20180131`
- Use data since 2018/01/31: `--timerange=20180131-`
- Use data since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
- Use data between POSIX / epoch timestamps 1527595200 1527618600: `--timerange=1527595200-1527618600`
## Understand the backtesting result
@ -248,19 +277,30 @@ A backtesting result will look like that:
| Max open trades | 3 |
| | |
| Total trades | 429 |
| Total Profit % | 152.41% |
| Starting balance | 0.01000000 BTC |
| Final balance | 0.01762792 BTC |
| Absolute profit | 0.00762792 BTC |
| Total profit % | 76.2% |
| Trades per day | 3.575 |
| Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC |
| | |
| Best Pair | LSK/BTC 26.26% |
| Worst Pair | ZEC/BTC -10.18% |
| Best Trade | LSK/BTC 4.25% |
| Worst Trade | ZEC/BTC -10.25% |
| Best day | 25.27% |
| Worst day | -30.67% |
| Best day | 0.00076 BTC |
| Worst day | -0.00036 BTC |
| Days win/draw/lose | 12 / 82 / 25 |
| Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 |
| | |
| Max Drawdown | 50.63% |
| Min balance | 0.00945123 BTC |
| Max balance | 0.01846651 BTC |
| Drawdown | 50.63% |
| Drawdown | 0.0015 BTC |
| Drawdown high | 0.0013 BTC |
| Drawdown low | -0.0002 BTC |
| Drawdown Start | 2019-02-15 14:10:00 |
| Drawdown End | 2019-04-11 18:15:00 |
| Market change | -5.88% |
@ -281,9 +321,9 @@ here:
The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has
earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC.
The column `avg profit %` shows the average profit for all trades made while the column `cum profit %` sums up all the profits/losses.
The column `tot profit %` shows instead the total profit % in relation to allocated capital (`max_open_trades * stake_amount`).
In the above results we have `max_open_trades=2` and `stake_amount=0.005` in config so `tot_profit %` will be `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`.
The column `Avg Profit %` shows the average profit for all trades made while the column `Cum Profit %` sums up all the profits/losses.
The column `Tot Profit %` shows instead the total profit % in relation to the starting balance.
In the above results, we have a starting balance of 0.01 BTC and an absolute profit of 0.00762792 BTC - so the `Tot Profit %` will be `(0.00762792 / 0.01) * 100 ~= 76.2%`.
Your strategy performance is influenced by your buy strategy, your sell strategy, and also by the `minimal_roi` and `stop_loss` you have set.
@ -324,19 +364,30 @@ It contains some useful key metrics about performance of your strategy on backte
| Max open trades | 3 |
| | |
| Total trades | 429 |
| Total Profit % | 152.41% |
| Starting balance | 0.01000000 BTC |
| Final balance | 0.01762792 BTC |
| Absolute profit | 0.00762792 BTC |
| Total profit % | 76.2% |
| Trades per day | 3.575 |
| Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC |
| | |
| Best Pair | LSK/BTC 26.26% |
| Worst Pair | ZEC/BTC -10.18% |
| Best Trade | LSK/BTC 4.25% |
| Worst Trade | ZEC/BTC -10.25% |
| Best day | 25.27% |
| Worst day | -30.67% |
| Best day | 0.00076 BTC |
| Worst day | -0.00036 BTC |
| Days win/draw/lose | 12 / 82 / 25 |
| Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 |
| | |
| Max Drawdown | 50.63% |
| Min balance | 0.00945123 BTC |
| Max balance | 0.01846651 BTC |
| Drawdown | 50.63% |
| Drawdown | 0.0015 BTC |
| Drawdown high | 0.0013 BTC |
| Drawdown low | -0.0002 BTC |
| Drawdown Start | 2019-02-15 14:10:00 |
| Drawdown End | 2019-04-11 18:15:00 |
| Market change | -5.88% |
@ -347,13 +398,21 @@ It contains some useful key metrics about performance of your strategy on backte
- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option).
- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower).
- `Total trades`: Identical to the total trades of the backtest output table.
- `Total Profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table.
- `Starting balance`: Start balance - as given by dry-run-wallet (config or command line).
- `Final balance`: Final balance - starting balance + absolute profit.
- `Absolute profit`: Profit made in stake currency.
- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital Starting capital) / Starting capital`.
- `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy).
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`.
- `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade
- `Best Trade` / `Worst Trade`: Biggest single winning trade and biggest single losing trade.
- `Best day` / `Worst day`: Best and worst day based on daily profit.
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
- `Max Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced).
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
- `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced).
- `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost.
- `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.
@ -418,6 +477,5 @@ Detailed output for all strategies one after the other will be available, so mak
## Next step
Great, your strategy is profitable. What if the bot can give your the
optimal parameters to use for your strategy?
Great, your strategy is profitable. What if the bot can give your the optimal parameters to use for your strategy?
Your next step is to learn [how to find optimal parameters with Hyperopt](hyperopt.md)

View File

@ -4,14 +4,14 @@ This page provides you some basic concepts on how Freqtrade works and operates.
## Freqtrade terminology
* Strategy: Your trading strategy, telling the bot what to do.
* Trade: Open position.
* 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).
* Timeframe: Candle length to use (e.g. `"5m"`, `"1h"`, ...).
* Indicators: Technical indicators (SMA, EMA, RSI, ...).
* Limit order: Limit orders which execute at the defined limit price or better.
* Market order: Guaranteed to fill, may move price depending on the order size.
* **Strategy**: Your trading strategy, telling the bot what to do.
* **Trade**: Open position.
* **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).
* **Timeframe**: Candle length to use (e.g. `"5m"`, `"1h"`, ...).
* **Indicators**: Technical indicators (SMA, EMA, RSI, ...).
* **Limit order**: Limit orders which execute at the defined limit price or better.
* **Market order**: Guaranteed to fill, may move price depending on the order size.
## Fee handling
@ -53,6 +53,7 @@ This loop will be repeated again and again until the bot is stopped.
* Calls `bot_loop_start()` once.
* Calculate indicators (calls `populate_indicators()` 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.
* Generate backtest report output

View File

@ -56,6 +56,7 @@ optional arguments:
usage: freqtrade trade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [-s NAME] [--strategy-path PATH]
[--db-url PATH] [--sd-notify] [--dry-run]
[--dry-run-wallet DRY_RUN_WALLET]
optional arguments:
-h, --help show this help message and exit
@ -66,6 +67,9 @@ optional arguments:
--sd-notify Notify systemd service manager.
--dry-run Enforce dry-run for trading (removes Exchange secrets
and simulates trades).
--dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET
Starting balance, used for backtesting / hyperopt and
dry-runs.
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).

View File

@ -40,8 +40,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| Parameter | Description |
|------------|-------------|
| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation which can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade).<br> **Datatype:** Positive integer or -1.
| `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Positive float or `"unlimited"`.
| `stake_currency` | **Required.** Crypto-currency used for trading. <br> **Datatype:** String
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float or `"unlimited"`.
| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`.
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio)
@ -49,7 +49,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `timeframe` | The timeframe (former ticker interval) to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
| `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
@ -58,15 +58,17 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling. <br> **Datatype:** Float (as ratio)
| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`).
| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook-enabled).
| `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled).
| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean
| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market) <br> *Defaults to `0`.* <br> **Datatype:** Float (as ratio)
| `ask_strategy.price_side` | Select the side of the spread the bot should look at to get the sell rate. [More information below](#sell-price-side).<br> *Defaults to `ask`.* <br> **Datatype:** String (either `ask` or `bid`).
| `ask_strategy.bid_last_balance` | Interpolate the selling price. More information [below](#sell-price-without-orderbook-enabled).
| `ask_strategy.use_order_book` | Enable selling of open trades using [Order Book Asks](#sell-price-with-orderbook-enabled). <br> **Datatype:** Boolean
| `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
@ -97,6 +99,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
| `webhook.enabled` | Enable usage of Webhook notifications <br> **Datatype:** Boolean
| `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
@ -141,8 +144,6 @@ Values set in the configuration file always overwrite values set in the strategy
* `process_only_new_candles`
* `order_types`
* `order_time_in_force`
* `stake_currency`
* `stake_amount`
* `unfilledtimeout`
* `disable_dataframe_checks`
* `protections`
@ -156,6 +157,23 @@ Values set in the configuration file always overwrite values set in the strategy
There are several methods to configure how much of the stake currency the bot will use to enter a trade. All methods respect the [available balance configuration](#available-balance) as explained below.
#### Minimum trade stake
The minimum stake amount will depend by exchange and pair, and is usually listed in the exchange support pages.
Assuming the minimum tradable amount for XRP/USD is 20 XRP (given by the exchange), and the price is 0.4$.
The minimum stake amount to buy this pair is therefore `20 * 0.6 ~= 12`.
This exchange has also a limit on USD - where all orders must be > 10$ - which however does not apply in this case.
To guarantee safe execution, freqtrade will not allow buying with a stake-amount of 10.1$, instead, it'll make sure that there's enough space to place a stoploss below the pair (+ an offset, defined by `amount_reserve_percent`, which defaults to 5%).
With a stoploss of 10% - we'd therefore end up with a value of ~13.8$ (`12 * (1 + 0.05 + 0.1)`).
To limit this calculation in case of large stoploss values, the calculated minimum stake-limit will never be more than 50% above the real limit.
!!! Warning
Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange.
#### Available balance
By default, the bot assumes that the `complete amount - 1%` is at it's disposal, and when using [dynamic stake amount](#dynamic-stake-amount), it will split the complete balance into `max_open_trades` buckets per trade.
@ -218,11 +236,14 @@ To allow the bot to trade all the available `stake_currency` in your account (mi
"tradable_balance_ratio": 0.99,
```
!!! Note
This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available).
!!! Tip "Compounding profits"
This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available), and will result in profit compounding.
!!! Note "When using Dry-Run Mode"
When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency.
When using `"stake_amount" : "unlimited",` in combination with Dry-Run, Backtesting or Hyperopt, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time.
It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency.
--8<-- "includes/pricing.md"
### Understand minimal_roi
@ -275,7 +296,7 @@ For example, if your strategy is using a 1h timeframe, and you only want to buy
### Understand order_types
The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`, `forcesell`, `forcebuy`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
This allows to buy using limit orders, sell using
limit-orders, and create stoplosses using market orders. It also allows to set the
@ -287,7 +308,7 @@ the buy order is fulfilled.
If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and
`stoploss_on_exchange`) need to be present, otherwise the bot will fail to start.
For information on (`emergencysell`,`stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md)
For information on (`emergencysell`,`forcesell`, `forcebuy`, `stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md)
Syntax for Strategy:
@ -296,6 +317,8 @@ order_types = {
"buy": "limit",
"sell": "limit",
"emergencysell": "market",
"forcebuy": "market",
"forcesell": "market",
"stoploss": "market",
"stoploss_on_exchange": False,
"stoploss_on_exchange_interval": 60,
@ -310,6 +333,8 @@ Configuration:
"buy": "limit",
"sell": "limit",
"emergencysell": "market",
"forcebuy": "market",
"forcesell": "market",
"stoploss": "market",
"stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60
@ -410,26 +435,6 @@ This configuration enables binance, as well as rate limiting to avoid bans from
Optimal settings for rate limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings.
We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step.
#### Advanced Freqtrade Exchange configuration
Advanced options can be configured using the `_ft_has_params` setting, which will override Defaults and exchange-specific behaviours.
Available options are listed in the exchange-class as `_ft_has_default`.
For example, to test the order type `FOK` with Kraken, and modify candle limit to 200 (so you only get 200 candles per API call):
```json
"exchange": {
"name": "kraken",
"_ft_has_params": {
"order_time_in_force": ["gtc", "fok"],
"ohlcv_candle_limit": 200
}
```
!!! Warning
Please make sure to fully understand the impacts of these settings before modifying them.
### What values can be used for fiat_display_currency?
The `fiat_display_currency` configuration parameter sets the base currency to use for the
@ -449,8 +454,6 @@ The valid values are:
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
```
--8<-- "includes/pricing.md"
## Using Dry-run mode
We recommend starting the bot in the Dry-run mode to see how your bot will

View File

@ -1,5 +1,7 @@
# Using Freqtrade with Docker
This page explains how to run the bot with Docker. It is not meant to work out of the box. You'll still need to read through the documentation and understand how to properly configure it.
## Install Docker
Start by downloading and installing Docker CE for your platform:
@ -75,7 +77,7 @@ The last 2 steps in the snippet create the directory with `user_data`, as well a
1. The configuration is now available as `user_data/config.json`
2. Copy a custom strategy to the directory `user_data/strategies/`
3. add the Strategy' class name to the `docker-compose.yml` file
3. Add the Strategy' class name to the `docker-compose.yml` file
The `SampleStrategy` is run by default.
@ -90,6 +92,9 @@ Once this is done, you're ready to launch the bot in trading mode (Dry-run or Li
docker-compose up -d
```
!!! Warning "Default configuration"
While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot.
#### Monitoring the bot
You can check for running instances with `docker-compose ps`.

View File

@ -40,6 +40,10 @@ Due to the heavy rate-limiting applied by Kraken, the following configuration se
},
```
!!! Warning "Downloading data from kraken"
Downloading kraken data will require significantly more memory (RAM) than any other exchange, as the trades-data needs to be converted into candles on your machine.
It will also take a long time, as freqtrade will need to download every single trade that happened on the exchange for the pair / timerange combination, therefore please be patient.
## Bittrex
### Order types
@ -92,9 +96,6 @@ To use subaccounts with FTX, you need to edit the configuration and add the foll
}
```
!!! Note
Older versions of freqtrade may require this key to be added to `"ccxt_async_config"` as well.
## All exchanges
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
@ -117,3 +118,23 @@ Whether your exchange returns incomplete candles or not can be checked using [th
Due to the danger of repainting, Freqtrade does not allow you to use this incomplete candle.
However, if it is based on the need for the latest price for your strategy - then this requirement can be acquired using the [data provider](strategy-customization.md#possible-options-for-dataprovider) from within the strategy.
### Advanced Freqtrade Exchange configuration
Advanced options can be configured using the `_ft_has_params` setting, which will override Defaults and exchange-specific behavior.
Available options are listed in the exchange-class as `_ft_has_default`.
For example, to test the order type `FOK` with Kraken, and modify candle limit to 200 (so you only get 200 candles per API call):
```json
"exchange": {
"name": "kraken",
"_ft_has_params": {
"order_time_in_force": ["gtc", "fok"],
"ohlcv_candle_limit": 200
}
```
!!! Warning
Please make sure to fully understand the impacts of these settings before modifying them.

View File

@ -43,7 +43,8 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--max-open-trades INT]
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
[--hyperopt NAME] [--hyperopt-path PATH] [--eps]
[--dmmp] [--enable-protections] [-e INT]
[--dmmp] [--enable-protections]
[--dry-run-wallet DRY_RUN_WALLET] [-e INT]
[--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]]
[--print-all] [--no-color] [--print-json] [-j JOBS]
[--random-state INT] [--min-trades INT]
@ -82,6 +83,9 @@ optional arguments:
Enable protections for backtesting.Will slow
backtesting down by a considerable amount, but will
include configured protections
--dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET
Starting balance, used for backtesting / hyperopt and
dry-runs.
-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} ...]
Specify which parameters to hyperopt. Space-separated
@ -161,7 +165,7 @@ Depending on the space you want to optimize, only some of the below are required
* fill `sell_indicator_space` - for sell signal optimization
!!! Note
`populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work.
`populate_indicators` needs to create all indicators any of the spaces may use, otherwise hyperopt will not work.
Optional in hyperopt - can also be loaded from a strategy (recommended):
@ -279,7 +283,7 @@ So let's write the buy strategy using these values:
"""
Define the buy strategy parameters to be used by Hyperopt.
"""
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = []
# GUARDS AND TRENDS
if 'adx-enabled' in params and params['adx-enabled']:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -103,6 +103,10 @@ A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting
When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price.
When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price.
The `ask_strategy.bid_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the last price and values between those interpolate between `side` and last price.
### Market order pricing
When using market orders, prices should be configured to use the "correct" side of the orderbook to allow realistic pricing detection.

View File

@ -5,12 +5,8 @@
<!-- Place this tag where you want the button to render. -->
<a class="github-button" href="https://github.com/freqtrade/freqtrade" data-icon="octicon-star" data-size="large" aria-label="Star freqtrade/freqtrade on GitHub">Star</a>
<!-- Place this tag where you want the button to render. -->
<a class="github-button" href="https://github.com/freqtrade/freqtrade/fork" data-icon="octicon-repo-forked" data-size="large" aria-label="Fork freqtrade/freqtrade on GitHub">Fork</a>
<!-- Place this tag where you want the button to render. -->
<a class="github-button" href="https://github.com/freqtrade/freqtrade/archive/stable.zip" data-icon="octicon-cloud-download" data-size="large" aria-label="Download freqtrade/freqtrade on GitHub">Download</a>
<!-- Place this tag where you want the button to render. -->
<a class="github-button" href="https://github.com/freqtrade" data-size="large" aria-label="Follow @freqtrade on GitHub">Follow @freqtrade</a>
## Introduction

View File

@ -6,22 +6,22 @@ This file was automatically generated - do not edit
{% set site_url = site_url ~ "/index.html" %}
{% endif %}
<header class="md-header" data-md-component="header">
<nav class="md-header-nav md-grid" aria-label="{{ lang.t('header.title') }}">
<a href="{{ site_url }}" title="{{ config.site_name | e }}" class="md-header-nav__button md-logo"
<nav class="md-header__inner md-grid" aria-label="{{ lang.t('header.title') }}">
<a href="{{ site_url }}" title="{{ config.site_name | e }}" class="md-header__button md-logo"
aria-label="{{ config.site_name }}">
{% include "partials/logo.html" %}
</a>
<label class="md-header-nav__button md-icon" for="__drawer">
<label class="md-header__button md-icon" for="__drawer">
{% include ".icons/material/menu" ~ ".svg" %}
</label>
<div class="md-header-nav__title" data-md-component="header-title">
<div class="md-header-nav__ellipsis">
<div class="md-header-nav__topic">
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
{{ config.site_name }}
</span>
</div>
<div class="md-header-nav__topic">
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
{% if page and page.meta and page.meta.title %}
{{ page.meta.title }}
@ -32,20 +32,41 @@ This file was automatically generated - do not edit
</div>
</div>
</div>
<div class="md-header__options">
{% if config.extra.alternate %}
<div class="md-select">
{% set icon = config.theme.icon.alternate or "material/translate" %}
<span class="md-header__button md-icon">
{% include ".icons/" ~ icon ~ ".svg" %}
</span>
<div class="md-select__inner">
<ul class="md-select__list">
{% for alt in config.extra.alternate %}
<li class="md-select__item">
<a href="{{ alt.link | url }}" class="md-select__link">
{{ alt.name }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</div>
{% if "search" in config["plugins"] %}
<label class="md-header-nav__button md-icon" for="__search">
<label class="md-header__button md-icon" for="__search">
{% include ".icons/material/magnify.svg" %}
</label>
{% include "partials/search.html" %}
{% endif %}
{% if config.repo_url %}
<div class="md-header-nav__source">
<div class="md-header__source">
{% include "partials/source.html" %}
</div>
{% endif %}
</nav>
<!-- Place this tag in your head or just before your close body tag. -->
<script async defer src="https://buttons.github.io/buttons.js"></script>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<!-- Place this tag in your head or just before your close body tag. -->
<script async defer src="https://buttons.github.io/buttons.js"></script>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
</header>

View File

@ -1,3 +1,3 @@
mkdocs-material==6.2.8
mkdocs-material==7.0.6
mdx_truly_sane_lists==1.2
pymdown-extensions==8.1.1

View File

@ -131,6 +131,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `status` | Lists all open trades.
| `count` | Displays number of trades used and available.
| `locks` | Displays currently locked pairs.
| `delete_lock <lock_id>` | Deletes (disables) the lock by id.
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance.
| `forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
| `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
@ -182,6 +183,11 @@ count
daily
Return the amount of open trades.
delete_lock
Delete (disable) lock from the database.
:param lock_id: ID for the lock to delete
delete_trade
Delete trade from the database.
Tries to close open orders. Requires manual handling of this asset on the exchange.
@ -202,6 +208,9 @@ forcesell
:param tradeid: Id of the trade (can be received via status command)
locks
Return current locks
logs
Show latest logs.

View File

@ -6,6 +6,10 @@ With some configuration, freqtrade (in combination with ccxt) provides access to
This document is an overview to configure Freqtrade to be used with sandboxes.
This can be useful to developers and trader alike.
!!! Warning
Sandboxes usually have very low volume, and either a very wide spread, or no orders available at all.
Therefore, sandboxes will usually not do a good job of showing you how a strategy would work in real trading.
## Exchanges known to have a sandbox / testnet
* [binance](https://testnet.binance.vision/)

View File

@ -51,6 +51,14 @@ The bot cannot do these every 5 seconds (at each iteration), otherwise it would
So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute).
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
### forcesell
`forcesell` is an optional value, which defaults to the same value as `sell` and is used when sending a `/forcesell` command from Telegram or from the Rest API.
### forcebuy
`forcebuy` is an optional value, which defaults to the same value as `buy` and is used when sending a `/forcebuy` command from Telegram or from the Rest API.
### emergencysell
`emergencysell` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails.

View File

@ -11,14 +11,73 @@ If you're just getting started, please be familiar with the methods described in
!!! Tip
You can get a strategy template containing all below methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced`
## Storing information
Storing information can be accomplished by creating a new dictionary within the strategy class.
The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables.
```python
class AwesomeStrategy(IStrategy):
# Create custom dictionary
custom_info = {}
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Check if the entry already exists
if not metadata["pair"] in self.custom_info:
# Create empty entry for this pair
self.custom_info[metadata["pair"]] = {}
if "crosstime" in self.custom_info[metadata["pair"]]:
self.custom_info[metadata["pair"]]["crosstime"] += 1
else:
self.custom_info[metadata["pair"]]["crosstime"] = 1
```
!!! Warning
The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash.
!!! Note
If the data is pair-specific, make sure to use pair as one of the keys in the dictionary.
***
### Storing custom information using DatetimeIndex from `dataframe`
Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps.
```python
import talib.abstract as ta
class AwesomeStrategy(IStrategy):
# Create custom dictionary
custom_info = {}
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# using "ATR" here as example
dataframe['atr'] = ta.ATR(dataframe)
if self.dp.runmode.value in ('backtest', 'hyperopt'):
# add indicator mapped to correct DatetimeIndex to custom_info
self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date')
return dataframe
```
!!! Warning
The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash.
!!! Note
If the data is pair-specific, make sure to use pair as one of the keys in the dictionary.
See `custom_stoploss` examples below on how to access the saved dataframe columns
## Custom stoploss
A stoploss can only ever move upwards - so if you set it to an absolute profit of 2%, you can never move it below this price.
Also, the traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss.
The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss.
The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object.
The method must return a stoploss value (float / number) with a relative ratio below the current price.
E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "locked in" a profit of 3% (`0.05 - 0.02 = 0.03`).
The method must return a stoploss value (float / number) as a percentage of the current price.
E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD.
The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price.
To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method:
@ -87,9 +146,9 @@ class AwesomeStrategy(IStrategy):
current_rate: float, current_profit: float, **kwargs) -> float:
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
if current_time - timedelta(minutes=120) > trade.open_date:
if current_time - timedelta(minutes=120) > trade.open_date_utc:
return -0.05
elif current_time - timedelta(minutes=60) > trade.open_date:
elif current_time - timedelta(minutes=60) > trade.open_date_utc:
return -0.10
return 1
```
@ -142,24 +201,32 @@ class AwesomeStrategy(IStrategy):
return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss
# After reaching the desired offset, allow the stoploss to trail by half the profit
desired_stoploss = current_profit / 2
desired_stoploss = current_profit / 2
# Use a minimum of 2.5% and a maximum of 5%
return max(min(desired_stoploss, 0.05), 0.025)
```
#### Absolute stoploss
#### Calculating stoploss relative to open price
The below example sets absolute profit levels based on the current profit.
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`.
#### Stepped stoploss
Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit.
* Use the regular stoploss until 20% profit is reached
* Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit.
* Once profit is > 25% - stoploss will be 15%.
* Once profit is > 20% - stoploss will be set to 7%.
* Once profit is > 20% - set stoploss to 7% above open price.
* Once profit is > 25% - set stoploss to 15% above open price.
* Once profit is > 40% - set stoploss to 25% above open price.
``` python
from datetime import datetime
from freqtrade.persistence import Trade
from freqtrade.strategy import stoploss_from_open
class AwesomeStrategy(IStrategy):
@ -170,15 +237,66 @@ class AwesomeStrategy(IStrategy):
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
# Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price
# evaluate highest to lowest, so that highest possible stop is used
if current_profit > 0.40:
return (-0.25 + current_profit)
if current_profit > 0.25:
return (-0.15 + current_profit)
if current_profit > 0.20:
return (-0.7 + current_profit)
return stoploss_from_open(0.25, current_profit)
elif current_profit > 0.25:
return stoploss_from_open(0.15, current_profit)
elif current_profit > 0.20:
return stoploss_from_open(0.07, current_profit)
# return maximum stoploss value, keeping current stoploss price unchanged
return 1
```
#### Custom stoploss using an indicator from dataframe example
Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR"
See: "Storing custom information using DatetimeIndex from `dataframe`" example above) on how to store the indicator into `custom_info`
!!! Warning
only use .iat[-1] in live mode, not in backtesting/hyperopt
otherwise you will look into the future
see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info.
``` python
from freqtrade.persistence import Trade
from freqtrade.state import RunMode
class AwesomeStrategy(IStrategy):
# ... populate_* methods
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
result = 1
if self.custom_info and pair in self.custom_info and trade:
# using current_time directly (like below) will only work in backtesting.
# so check "runmode" to make sure that it's only used in backtesting/hyperopt
if self.dp and self.dp.runmode.value in ('backtest', 'hyperopt'):
relative_sl = self.custom_info[pair].loc[current_time]['atr']
# in live / dry-run, it'll be really the current time
else:
# but we can just use the last entry from an already analyzed dataframe instead
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
timeframe=self.timeframe)
# WARNING
# only use .iat[-1] in live mode, not in backtesting/hyperopt
# otherwise you will look into the future
# see: https://www.freqtrade.io/en/latest/strategy-customization/#common-mistakes-when-developing-strategies
relative_sl = dataframe['atr'].iat[-1]
if (relative_sl is not None):
# new stoploss relative to current_rate
new_stoploss = (current_rate-relative_sl)/current_rate
# turn into relative negative offset required by `custom_stoploss` return implementation
result = new_stoploss - 1
return result
```
---
@ -199,7 +317,7 @@ It applies a tight timeout for higher priced assets, while allowing more time to
The function must return either `True` (cancel order) or `False` (keep order alive).
``` python
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy):
@ -213,21 +331,21 @@ class AwesomeStrategy(IStrategy):
}
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date < datetime.utcnow() - timedelta(minutes=5):
if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5):
return True
elif trade.open_rate > 10 and trade.open_date < datetime.utcnow() - timedelta(minutes=3):
elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3):
return True
elif trade.open_rate < 1 and trade.open_date < datetime.utcnow() - timedelta(hours=24):
elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24):
return True
return False
def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date < datetime.utcnow() - timedelta(minutes=5):
if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5):
return True
elif trade.open_rate > 10 and trade.open_date < datetime.utcnow() - timedelta(minutes=3):
elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3):
return True
elif trade.open_rate < 1 and trade.open_date < datetime.utcnow() - timedelta(hours=24):
elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24):
return True
return False
```

View File

@ -300,38 +300,7 @@ The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `p
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
The Metadata-dict should not be modified and does not persist information across multiple calls.
Instead, have a look at the section [Storing information](#Storing-information)
### Storing information
Storing information can be accomplished by creating a new dictionary within the strategy class.
The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables.
```python
class AwesomeStrategy(IStrategy):
# Create custom dictionary
cust_info = {}
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Check if the entry already exists
if not metadata["pair"] in self.cust_info:
# Create empty entry for this pair
self.cust_info[metadata["pair"]] = {}
if "crosstime" in self.cust_info[metadata["pair"]]:
self.cust_info[metadata["pair"]]["crosstime"] += 1
else:
self.cust_info[metadata["pair"]]["crosstime"] = 1
```
!!! Warning
The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash.
!!! Note
If the data is pair-specific, make sure to use pair as one of the keys in the dictionary.
***
Instead, have a look at the section [Storing information](strategy-advanced.md#Storing-information)
## Additional data (informative_pairs)
@ -399,7 +368,7 @@ if self.dp:
### *current_whitelist()*
Imagine you've developed a strategy that trades the `5m` timeframe using signals generated from a `1d` timeframe on the top 10 volume pairs by volume.
Imagine you've developed a strategy that trades the `5m` timeframe using signals generated from a `1d` timeframe on the top 10 volume pairs by volume.
The strategy might look something like this:
@ -418,7 +387,7 @@ This is where calling `self.dp.current_whitelist()` comes in handy.
pairs = self.dp.current_whitelist()
# Assign tf to each pair so they can be downloaded and cached for strategy.
informative_pairs = [(pair, '1d') for pair in pairs]
return informative_pairs
return informative_pairs
```
### *get_pair_dataframe(pair, timeframe)*
@ -467,6 +436,26 @@ if self.dp:
dataframe['best_ask'] = ob['asks'][0][0]
```
The orderbook structure is aligned with the order structure from [ccxt](https://github.com/ccxt/ccxt/wiki/Manual#order-book-structure), so the result will look as follows:
``` js
{
'bids': [
[ price, amount ], // [ float, float ]
[ price, amount ],
...
],
'asks': [
[ price, amount ],
[ price, amount ],
//...
],
//...
}
```
Therefore, using `ob['bids'][0][0]` as demonstrated above will result in using the best bid price. `ob['bids'][0][1]` would look at the amount at this orderbook position.
!!! Warning "Warning about backtesting"
The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used, as the method will return uptodate values.
@ -583,7 +572,7 @@ All columns of the informative dataframe will be available on the returning data
``` python
'date', 'open', 'high', 'low', 'close', 'rsi' # from the original dataframe
'date_1h', 'open_1h', 'high_1h', 'low_1h', 'close_1h', 'rsi_1h' # from the informative dataframe
'date_1h', 'open_1h', 'high_1h', 'low_1h', 'close_1h', 'rsi_1h' # from the informative dataframe
```
??? Example "Custom implementation"
@ -618,6 +607,43 @@ All columns of the informative dataframe will be available on the returning data
***
### *stoploss_from_open()*
Stoploss values returned from `custom_stoploss` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the open price instead. `stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired percentage above the open price.
??? Example "Returning a stoploss relative to the open price from the custom stoploss function"
Say the open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`).
If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100.
``` python
from datetime import datetime
from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, stoploss_from_open
class AwesomeStrategy(IStrategy):
# ... populate_* methods
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
# once the profit has risin above 10%, keep the stoploss at 7% above the open price
if current_profit > 0.10:
return stoploss_from_open(0.07, current_profit)
return 1
```
Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation.
## Additional data (Wallets)
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
@ -709,7 +735,7 @@ To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished.
!!! Warning
Locking pairs is not available during backtesting.
Manually locking pairs is not available during backtesting, only locks via Protections are allowed.
#### Pair locking example

View File

@ -83,10 +83,13 @@ Example configuration showing the different settings:
"sell": "on",
"buy_cancel": "silent",
"sell_cancel": "on"
}
},
"balance_dust_level": 0.01
},
```
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
## Create a custom keyboard (command shortcut buttons)
Telegram allows us to create a custom keyboard with buttons for commands.
@ -143,6 +146,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
| `/count` | Displays number of trades used and available
| `/locks` | Show currently locked pairs.
| `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id).
| `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance
| `/forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
| `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).

View File

@ -40,6 +40,21 @@ Sample configuration (tested using IFTTT).
The url in `webhook.url` should point to the correct url for your webhook. If you're using [IFTTT](https://ifttt.com) (as shown in the sample above) please insert our event and key to the url.
You can set the POST body format to Form-Encoded (default) or JSON-Encoded. Use `"format": "form"` or `"format": "json"` respectively. Example configuration for Mattermost Cloud integration:
```json
"webhook": {
"enabled": true,
"url": "https://<YOURSUBDOMAIN>.cloud.mattermost.com/hooks/<YOURHOOK>",
"format": "json",
"webhookstatus": {
"text": "Status: {status}"
}
},
```
The result would be POST request with e.g. `{"text":"Status: running"}` body and `Content-Type: application/json` header which results `Status: running` message in the Mattermost channel.
Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called.
### Webhookbuy

View File

@ -1,5 +1,5 @@
""" Freqtrade bot """
__version__ = '2021.2'
__version__ = '2021.3'
if __version__ == 'develop':

View File

@ -14,18 +14,18 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_dat
ARGS_STRATEGY = ["strategy", "strategy_path"]
ARGS_TRADE = ["db_url", "sd_notify", "dry_run"]
ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"]
ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv",
"max_open_trades", "stake_amount", "fee"]
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
"enable_protections",
"enable_protections", "dry_run_wallet",
"strategy_list", "export", "exportfilename"]
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
"position_stacking", "use_max_market_positions",
"enable_protections",
"enable_protections", "dry_run_wallet",
"epochs", "spaces", "print_all",
"print_colorized", "print_json", "hyperopt_jobs",
"hyperopt_random_state", "hyperopt_min_trades",

View File

@ -93,10 +93,10 @@ def ask_user_config() -> Dict[str, Any]:
"message": "Select exchange",
"choices": [
"binance",
"binanceje",
"binanceus",
"bittrex",
"kraken",
"ftx",
Separator(),
"other",
],
@ -173,6 +173,9 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
arguments=selections)
logger.info(f"Writing config to `{config_path}`.")
logger.info(
"Please make sure to check the configuration contents and adjust settings to your needs.")
config_path.write_text(config_text)

View File

@ -110,6 +110,11 @@ AVAILABLE_CLI_OPTIONS = {
help='Enforce dry-run for trading (removes Exchange secrets and simulates trades).',
action='store_true',
),
"dry_run_wallet": Arg(
'--dry-run-wallet', '--starting-balance',
help='Starting balance, used for backtesting / hyperopt and dry-runs.',
type=float,
),
# Optimize common
"timeframe": Arg(
'-i', '--timeframe', '--ticker-interval',
@ -128,7 +133,6 @@ AVAILABLE_CLI_OPTIONS = {
"stake_amount": Arg(
'--stake-amount',
help='Override the value of the `stake_amount` configuration setting.',
type=float,
),
# Backtesting
"position_stacking": Arg(

View File

@ -17,7 +17,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
"""
List hyperopt epochs previously evaluated
"""
from freqtrade.optimize.hyperopt import Hyperopt
from freqtrade.optimize.hyperopt_tools import HyperoptTools
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
@ -47,7 +47,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
config.get('hyperoptexportfilename'))
# Previous evaluations
epochs = Hyperopt.load_previous_results(results_file)
epochs = HyperoptTools.load_previous_results(results_file)
total_epochs = len(epochs)
epochs = hyperopt_filter_epochs(epochs, filteroptions)
@ -57,18 +57,19 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
if not export_csv:
try:
print(Hyperopt.get_result_table(config, epochs, total_epochs,
not filteroptions['only_best'], print_colorized, 0))
print(HyperoptTools.get_result_table(config, epochs, total_epochs,
not filteroptions['only_best'],
print_colorized, 0))
except KeyboardInterrupt:
print('User interrupted..')
if epochs and not no_details:
sorted_epochs = sorted(epochs, key=itemgetter('loss'))
results = sorted_epochs[0]
Hyperopt.print_epoch_details(results, total_epochs, print_json, no_header)
HyperoptTools.print_epoch_details(results, total_epochs, print_json, no_header)
if epochs and export_csv:
Hyperopt.export_csv_file(
HyperoptTools.export_csv_file(
config, epochs, total_epochs, not filteroptions['only_best'], export_csv
)
@ -77,7 +78,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
"""
Show details of a hyperopt epoch previously evaluated
"""
from freqtrade.optimize.hyperopt import Hyperopt
from freqtrade.optimize.hyperopt_tools import HyperoptTools
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
@ -105,7 +106,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
}
# Previous evaluations
epochs = Hyperopt.load_previous_results(results_file)
epochs = HyperoptTools.load_previous_results(results_file)
total_epochs = len(epochs)
epochs = hyperopt_filter_epochs(epochs, filteroptions)
@ -124,8 +125,8 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
if epochs:
val = epochs[n]
Hyperopt.print_epoch_details(val, total_epochs, print_json, no_header,
header_str="Epoch details")
HyperoptTools.print_epoch_details(val, total_epochs, print_json, no_header,
header_str="Epoch details")
def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:

View File

@ -3,7 +3,8 @@ from typing import Any, Dict
from freqtrade import constants
from freqtrade.configuration import setup_utils_configuration
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exceptions import OperationalException
from freqtrade.misc import round_coin_value
from freqtrade.state import RunMode
@ -22,11 +23,13 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
RunMode.BACKTEST: 'backtesting',
RunMode.HYPEROPT: 'hyperoptimization',
}
if (method in no_unlimited_runmodes.keys() and
config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT):
raise DependencyException(
f'The value of `stake_amount` cannot be set as "{constants.UNLIMITED_STAKE_AMOUNT}" '
f'for {no_unlimited_runmodes[method]}')
if method in no_unlimited_runmodes.keys():
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT
and config['stake_amount'] > config['dry_run_wallet']):
wallet = round_coin_value(config['dry_run_wallet'], config['stake_currency'])
stake = round_coin_value(config['stake_amount'], config['stake_currency'])
raise OperationalException(f"Starting balance ({wallet}) "
f"is smaller than stake_amount {stake}.")
return config

View File

@ -47,6 +47,8 @@ def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]:
conf_schema = deepcopy(constants.CONF_SCHEMA)
if conf.get('runmode', RunMode.OTHER) in (RunMode.DRY_RUN, RunMode.LIVE):
conf_schema['required'] = constants.SCHEMA_TRADE_REQUIRED
elif conf.get('runmode', RunMode.OTHER) in (RunMode.BACKTEST, RunMode.HYPEROPT):
conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED
else:
conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED
try:
@ -72,6 +74,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None:
# validating trailing stoploss
_validate_trailing_stoploss(conf)
_validate_price_config(conf)
_validate_edge(conf)
_validate_whitelist(conf)
_validate_protections(conf)
@ -93,6 +96,19 @@ def _validate_unlimited_amount(conf: Dict[str, Any]) -> None:
raise OperationalException("`max_open_trades` and `stake_amount` cannot both be unlimited.")
def _validate_price_config(conf: Dict[str, Any]) -> None:
"""
When using market orders, price sides must be using the "other" side of the price
"""
if (conf.get('order_types', {}).get('buy') == 'market'
and conf.get('bid_strategy', {}).get('price_side') != 'ask'):
raise OperationalException('Market buy orders require bid_strategy.price_side = "ask".')
if (conf.get('order_types', {}).get('sell') == 'market'
and conf.get('ask_strategy', {}).get('price_side') != 'bid'):
raise OperationalException('Market sell orders require ask_strategy.price_side = "bid".')
def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
if conf.get('stoploss') == 0.0:

View File

@ -214,9 +214,6 @@ class Configuration:
self._args_to_config(
config, argname='enable_protections',
logstring='Parameter --enable-protections detected, enabling Protections. ...')
# Setting max_open_trades to infinite if -1
if config.get('max_open_trades') == -1:
config['max_open_trades'] = float('inf')
if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]:
config.update({'use_max_market_positions': False})
@ -228,11 +225,23 @@ class Configuration:
'overriding max_open_trades to: %s ...', config.get('max_open_trades'))
elif config['runmode'] in NON_UTIL_MODES:
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
# Setting max_open_trades to infinite if -1
if config.get('max_open_trades') == -1:
config['max_open_trades'] = float('inf')
if self.args.get('stake_amount', None):
# Convert explicitly to float to support CLI argument for both unlimited and value
try:
self.args['stake_amount'] = float(self.args['stake_amount'])
except ValueError:
pass
self._args_to_config(config, argname='stake_amount',
logstring='Parameter --stake-amount detected, '
'overriding stake_amount to: {} ...')
self._args_to_config(config, argname='dry_run_wallet',
logstring='Parameter --dry-run-wallet detected, '
'overriding dry_run_wallet to: {} ...')
self._args_to_config(config, argname='fee',
logstring='Parameter --fee detected, '
'setting fee to: {} ...')

View File

@ -7,6 +7,8 @@ from typing import Optional
import arrow
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__)
@ -103,5 +105,8 @@ class TimeRange:
stop = int(stops) // 1000
else:
stop = int(stops)
if start > stop > 0:
raise OperationalException(
f'Start date is after stop date for timerange "{text}"')
return TimeRange(stype[0], stype[1], start, stop)
raise Exception('Incorrect syntax for timerange "%s"' % text)
raise OperationalException(f'Incorrect syntax for timerange "{text}"')

View File

@ -54,6 +54,11 @@ DECIMALS_PER_COIN = {
'ETH': 5,
}
DUST_PER_COIN = {
'BTC': 0.0001,
'ETH': 0.01
}
# Soure files with destination directories within user-directory
USER_DATA_FILES = {
@ -160,6 +165,12 @@ CONF_SCHEMA = {
'type': 'object',
'properties': {
'price_side': {'type': 'string', 'enum': ORDERBOOK_SIDES, 'default': 'ask'},
'bid_last_balance': {
'type': 'number',
'minimum': 0,
'maximum': 1,
'exclusiveMaximum': False,
},
'use_order_book': {'type': 'boolean'},
'order_book_min': {'type': 'integer', 'minimum': 1},
'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50},
@ -174,6 +185,8 @@ CONF_SCHEMA = {
'properties': {
'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'forcesell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'forcebuy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss_on_exchange': {'type': 'boolean'},
@ -230,6 +243,7 @@ CONF_SCHEMA = {
'enabled': {'type': 'boolean'},
'token': {'type': 'string'},
'chat_id': {'type': 'string'},
'balance_dust_level': {'type': 'number', 'minimum': 0.0},
'notification_settings': {
'type': 'object',
'properties': {
@ -243,7 +257,7 @@ CONF_SCHEMA = {
}
}
},
'required': ['enabled', 'token', 'chat_id']
'required': ['enabled', 'token', 'chat_id'],
},
'webhook': {
'type': 'object',
@ -370,6 +384,16 @@ SCHEMA_TRADE_REQUIRED = [
'dataformat_trades',
]
SCHEMA_BACKTEST_REQUIRED = [
'exchange',
'max_open_trades',
'stake_currency',
'stake_amount',
'dry_run_wallet',
'dataformat_ohlcv',
'dataformat_trades',
]
SCHEMA_MINIMAL_REQUIRED = [
'exchange',
'dry_run',

View File

@ -10,7 +10,7 @@ import pandas as pd
from freqtrade.constants import LAST_BT_RESULT_FN
from freqtrade.misc import json_load
from freqtrade.persistence import Trade, init_db
from freqtrade.persistence import LocalTrade, Trade, init_db
logger = logging.getLogger(__name__)
@ -224,7 +224,7 @@ def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
return df_final[df_final['open_trades'] > max_open_trades]
def trade_list_to_dataframe(trades: List[Trade]) -> pd.DataFrame:
def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame:
"""
Convert list of Trade objects to pandas Dataframe
:param trades: List of trade objects
@ -360,13 +360,14 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date',
value_col: str = 'profit_ratio'
) -> Tuple[float, pd.Timestamp, pd.Timestamp]:
) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float]:
"""
Calculate max drawdown and the corresponding close dates
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
:param date_col: Column in DataFrame to use for dates (defaults to 'close_date')
:param value_col: Column in DataFrame to use for values (defaults to 'profit_ratio')
:return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time
:return: Tuple (float, highdate, lowdate, highvalue, lowvalue) with absolute max drawdown,
high and low time and high and low value.
:raise: ValueError if trade-dataframe was found empty.
"""
if len(trades) == 0:
@ -382,13 +383,17 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date'
raise ValueError("No losing trade, therefore no drawdown.")
high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col]
low_date = profit_results.loc[idxmin, date_col]
return abs(min(max_drawdown_df['drawdown'])), high_date, low_date
high_val = max_drawdown_df.loc[max_drawdown_df.iloc[:idxmin]
['high_value'].idxmax(), 'cumulative']
low_val = max_drawdown_df.loc[idxmin, 'cumulative']
return abs(min(max_drawdown_df['drawdown'])), high_date, low_date, high_val, low_val
def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]:
def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> Tuple[float, float]:
"""
Calculate min/max cumsum of trades, to show if the wallet/stake amount ratio is sane
:param trades: DataFrame containing trades (requires columns close_date and profit_percent)
:param starting_balance: Add starting balance to results, to show the wallets high / low points
:return: Tuple (float, float) with cumsum of profit_abs
:raise: ValueError if trade-dataframe was found empty.
"""
@ -397,7 +402,7 @@ def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]:
csum_df = pd.DataFrame()
csum_df['sum'] = trades['profit_abs'].cumsum()
csum_min = csum_df['sum'].min()
csum_max = csum_df['sum'].max()
csum_min = csum_df['sum'].min() + starting_balance
csum_max = csum_df['sum'].max() + starting_balance
return csum_min, csum_max

View File

@ -147,6 +147,9 @@ class Exchange:
"""
Destructor - clean up async stuff
"""
self.close()
def close(self):
logger.debug("Exchange object destroyed, closing async loop")
if self._api_async and inspect.iscoroutinefunction(self._api_async.close):
asyncio.get_event_loop().run_until_complete(self._api_async.close())
@ -308,8 +311,8 @@ class Exchange:
self._markets = self._api.load_markets()
self._load_async_markets()
self._last_markets_refresh = arrow.utcnow().int_timestamp
except ccxt.BaseError as e:
logger.warning('Unable to initialize markets. Reason: %s', e)
except ccxt.BaseError:
logger.exception('Unable to initialize markets.')
def reload_markets(self) -> None:
"""Reload markets both sync and async if refresh interval has passed """
@ -528,16 +531,16 @@ class Exchange:
return None
# reserve some percent defined in config (5% default) + stoploss
amount_reserve_percent = 1.0 - self._config.get('amount_reserve_percent',
amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent',
DEFAULT_AMOUNT_RESERVE_PERCENT)
amount_reserve_percent += stoploss
amount_reserve_percent += abs(stoploss)
# it should not be more than 50%
amount_reserve_percent = max(amount_reserve_percent, 0.5)
amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1)
# The value returned should satisfy both limits: for amount (base currency) and
# for cost (quote, stake currency), so max() is used here.
# See also #2575 at github.
return max(min_stake_amounts) / amount_reserve_percent
return max(min_stake_amounts) * amount_reserve_percent
def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
rate: float, params: Dict = {}) -> Dict[str, Any]:
@ -1053,7 +1056,8 @@ class Exchange:
:param order: Order dict as returned from fetch_order()
:return: True if order has been cancelled without being filled, False otherwise.
"""
return order.get('status') in ('closed', 'canceled') and order.get('filled') == 0.0
return (order.get('status') in ('closed', 'canceled', 'cancelled')
and order.get('filled') == 0.0)
@retrier
def cancel_order(self, order_id: str, pair: str) -> Dict:
@ -1228,6 +1232,8 @@ class Exchange:
def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
price: float = 1, taker_or_maker: str = 'maker') -> float:
try:
if self._config['dry_run'] and self._config.get('fee', None) is not None:
return self._config['fee']
# validate that markets are loaded before trying to get fee
if self._api.markets is None or len(self._api.markets) == 0:
self._api.load_markets()

View File

@ -432,7 +432,7 @@ class FreqtradeBot(LoggingMixin):
ticker = self.exchange.fetch_ticker(pair)
ticker_rate = ticker[bid_strategy['price_side']]
if ticker['last'] and ticker_rate > ticker['last']:
balance = self.config['bid_strategy']['ask_last_balance']
balance = bid_strategy['ask_last_balance']
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
used_rate = ticker_rate
@ -520,7 +520,8 @@ class FreqtradeBot(LoggingMixin):
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
return False
def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None) -> bool:
def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None,
forcebuy: bool = False) -> bool:
"""
Executes a limit buy for the given pair
:param pair: pair for which we want to create a LIMIT_BUY
@ -548,6 +549,10 @@ class FreqtradeBot(LoggingMixin):
amount = stake_amount / buy_limit_requested
order_type = self.strategy.order_types['buy']
if forcebuy:
# Forcebuy can define a different ordertype
order_type = self.strategy.order_types.get('forcebuy', order_type)
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested,
time_in_force=time_in_force):
@ -740,7 +745,13 @@ class FreqtradeBot(LoggingMixin):
logger.warning("Sell Price at location from orderbook could not be determined.")
raise PricingError from e
else:
rate = self.exchange.fetch_ticker(pair)[ask_strategy['price_side']]
ticker = self.exchange.fetch_ticker(pair)
ticker_rate = ticker[ask_strategy['price_side']]
if ticker['last'] and ticker_rate < ticker['last']:
balance = ask_strategy.get('bid_last_balance', 0.0)
ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last'])
rate = ticker_rate
if rate is None:
raise PricingError(f"Sell-Rate for {pair} was empty.")
self._sell_rate_cache[pair] = rate
@ -932,7 +943,7 @@ class FreqtradeBot(LoggingMixin):
Check and execute sell
"""
should_sell = self.strategy.should_sell(
trade, sell_rate, datetime.utcnow(), buy, sell,
trade, sell_rate, datetime.now(timezone.utc), buy, sell,
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
)
@ -1018,13 +1029,13 @@ class FreqtradeBot(LoggingMixin):
was_trade_fully_canceled = False
# Cancelled orders may have the status of 'canceled' or 'closed'
if order['status'] not in ('canceled', 'closed'):
if order['status'] not in ('cancelled', 'canceled', 'closed'):
corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
trade.amount)
# 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
# handled in the next iteration.
if corder.get('status') not in ('canceled', 'closed'):
if corder.get('status') not in ('cancelled', 'canceled', 'closed'):
logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.")
return False
else:
@ -1156,6 +1167,10 @@ class FreqtradeBot(LoggingMixin):
if sell_reason == SellType.EMERGENCY_SELL:
# Emergency sells (default to market!)
order_type = self.strategy.order_types.get("emergencysell", "market")
if sell_reason == SellType.FORCE_SELL:
# Force sells (default to the sell_type defined in the strategy,
# but we allow this value to be changed)
order_type = self.strategy.order_types.get("forcesell", order_type)
amount = self._safe_sell_amount(trade.pair, trade.amount)
time_in_force = self.strategy.order_time_in_force['sell']

View File

@ -17,17 +17,18 @@ from freqtrade.data import history
from freqtrade.data.btanalysis import trade_list_to_dataframe
from freqtrade.data.converter import trim_dataframe
from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import OperationalException
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.mixins import LoggingMixin
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
store_backtest_stats)
from freqtrade.persistence import PairLocks, Trade
from freqtrade.persistence import LocalTrade, PairLocks, Trade
from freqtrade.plugins.pairlistmanager import PairListManager
from freqtrade.plugins.protectionmanager import ProtectionManager
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.wallets import Wallets
logger = logging.getLogger(__name__)
@ -114,6 +115,8 @@ class Backtesting:
if self.config.get('enable_protections', False):
self.protections = ProtectionManager(self.config)
self.wallets = Wallets(self.config, self.exchange, log=False)
# Get maximum required startup period
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
# Load one (first) strategy
@ -124,7 +127,7 @@ class Backtesting:
PairLocks.use_db = True
Trade.use_db = True
def _set_strategy(self, strategy):
def _set_strategy(self, strategy: IStrategy):
"""
Load strategy into backtesting
"""
@ -171,10 +174,8 @@ class Backtesting:
PairLocks.use_db = False
PairLocks.timeframe = self.config['timeframe']
Trade.use_db = False
if enable_protections:
# Reset persisted data - used for protections only
PairLocks.reset_locks()
Trade.reset_trades()
PairLocks.reset_locks()
Trade.reset_trades()
def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]:
"""
@ -203,10 +204,10 @@ class Backtesting:
# Convert from Pandas to list for performance reasons
# (Looping Pandas is slow.)
data[pair] = [x for x in df_analyzed.itertuples(index=False, name=None)]
data[pair] = df_analyzed.values.tolist()
return data
def _get_close_rate(self, sell_row: Tuple, trade: Trade, sell: SellCheckTuple,
def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple,
trade_dur: int) -> float:
"""
Get close rate for backtesting result
@ -246,24 +247,67 @@ class Backtesting:
else:
return sell_row[OPEN_IDX]
def _get_sell_trade_entry(self, trade: Trade, sell_row: Tuple) -> Optional[Trade]:
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], sell_row[DATE_IDX],
sell_row[BUY_IDX], sell_row[SELL_IDX],
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore
sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX],
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
if sell.sell_flag:
trade_dur = int((sell_row[DATE_IDX] - trade.open_date).total_seconds() // 60)
trade.close_date = sell_row[DATE_IDX]
trade.sell_reason = sell.sell_type.value
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)
trade.close_date = sell_row[DATE_IDX]
trade.sell_reason = sell.sell_type
# Confirm trade exit:
time_in_force = self.strategy.order_time_in_force['sell']
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
rate=closerate,
time_in_force=time_in_force,
sell_reason=sell.sell_type.value):
return None
trade.close(closerate, show_msg=False)
return trade
return None
def handle_left_open(self, open_trades: Dict[str, List[Trade]],
data: Dict[str, List[Tuple]]) -> List[Trade]:
def _enter_trade(self, pair: str, row: List, max_open_trades: int,
open_trade_count: int) -> Optional[LocalTrade]:
try:
stake_amount = self.wallets.get_trade_stake_amount(
pair, max_open_trades - open_trade_count, None)
except DependencyException:
return None
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05)
order_type = self.strategy.order_types['buy']
time_in_force = self.strategy.order_time_in_force['sell']
# Confirm trade entry:
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX],
time_in_force=time_in_force):
return None
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
# Enter trade
trade = LocalTrade(
pair=pair,
open_rate=row[OPEN_IDX],
open_date=row[DATE_IDX],
stake_amount=stake_amount,
amount=round(stake_amount / row[OPEN_IDX], 8),
fee_open=self.fee,
fee_close=self.fee,
is_open=True,
exchange='backtesting',
)
return trade
return None
def handle_left_open(self, open_trades: Dict[str, List[LocalTrade]],
data: Dict[str, List[Tuple]]) -> List[LocalTrade]:
"""
Handling of left open trades at the end of backtesting
"""
@ -274,13 +318,16 @@ class Backtesting:
sell_row = data[pair][-1]
trade.close_date = sell_row[DATE_IDX]
trade.sell_reason = SellType.FORCE_SELL
trade.sell_reason = SellType.FORCE_SELL.value
trade.close(sell_row[OPEN_IDX], show_msg=False)
trade.is_open = True
trades.append(trade)
LocalTrade.close_bt_trade(trade)
# Deepcopy object to have wallets update correctly
trade1 = deepcopy(trade)
trade1.is_open = True
trades.append(trade1)
return trades
def backtest(self, processed: Dict, stake_amount: float,
def backtest(self, processed: Dict,
start_date: datetime, end_date: datetime,
max_open_trades: int = 0, position_stacking: bool = False,
enable_protections: bool = False) -> DataFrame:
@ -292,7 +339,6 @@ class Backtesting:
Avoid extensive logging in this method and functions it calls.
:param processed: a processed dictionary with format {pair, data}
:param stake_amount: amount to use for each trade
:param start_date: backtesting timerange start datetime
:param end_date: backtesting timerange end datetime
:param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited
@ -300,11 +346,7 @@ class Backtesting:
:param enable_protections: Should protections be enabled?
:return: DataFrame with trades (results of backtesting)
"""
logger.debug(f"Run backtest, stake_amount: {stake_amount}, "
f"start_date: {start_date}, end_date: {end_date}, "
f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}"
)
trades: List[Trade] = []
trades: List[LocalTrade] = []
self.prepare_backtest(enable_protections)
# Use dict of lists with data for performance
@ -315,7 +357,7 @@ class Backtesting:
indexes: Dict = {}
tmp = start_date + timedelta(minutes=self.timeframe_min)
open_trades: Dict[str, List] = defaultdict(list)
open_trades: Dict[str, List[LocalTrade]] = defaultdict(list)
open_trade_count = 0
# Loop timerange and get candle for each pair at that point in time
@ -346,28 +388,18 @@ class Backtesting:
and tmp != end_date
and row[BUY_IDX] == 1 and row[SELL_IDX] != 1
and not PairLocks.is_pair_locked(pair, row[DATE_IDX])):
# Enter trade
trade = Trade(
pair=pair,
open_rate=row[OPEN_IDX],
open_date=row[DATE_IDX],
stake_amount=stake_amount,
amount=round(stake_amount / row[OPEN_IDX], 8),
fee_open=self.fee,
fee_close=self.fee,
is_open=True,
)
# TODO: hacky workaround to avoid opening > max_open_trades
# This emulates previous behaviour - not sure if this is correct
# Prevents buying if the trade-slot was freed in this candle
open_trade_count_start += 1
open_trade_count += 1
# logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.")
open_trades[pair].append(trade)
Trade.trades.append(trade)
trade = self._enter_trade(pair, row, max_open_trades, open_trade_count_start)
if trade:
# TODO: hacky workaround to avoid opening > max_open_trades
# This emulates previous behaviour - not sure if this is correct
# Prevents buying if the trade-slot was freed in this candle
open_trade_count_start += 1
open_trade_count += 1
# logger.debug(f"{pair} - Emulate creation of new trade: {trade}.")
open_trades[pair].append(trade)
LocalTrade.add_bt_trade(trade)
for trade in open_trades[pair]:
# since indexes has been incremented before, we need to go one step back to
# also check the buying candle for sell conditions.
trade_entry = self._get_sell_trade_entry(trade, row)
# Sell occured
@ -375,6 +407,8 @@ class Backtesting:
# logger.debug(f"{pair} - Backtesting sell {trade}")
open_trade_count -= 1
open_trades[pair].remove(trade)
LocalTrade.close_bt_trade(trade)
trades.append(trade_entry)
if enable_protections:
self.protections.stop_per_pair(pair, row[DATE_IDX])
@ -384,6 +418,7 @@ class Backtesting:
tmp += timedelta(minutes=self.timeframe_min)
trades += self.handle_left_open(open_trades, data=data)
self.wallets.update()
return trade_list_to_dataframe(trades)
@ -417,7 +452,6 @@ class Backtesting:
# Execute backtest and store results
results = self.backtest(
processed=preprocessed,
stake_amount=self.config['stake_amount'],
start_date=min_date.datetime,
end_date=max_date.datetime,
max_open_trades=max_open_trades,
@ -428,7 +462,8 @@ class Backtesting:
self.all_results[self.strategy.get_strategy_name()] = {
'results': results,
'config': self.strategy.config,
'locks': PairLocks.locks,
'locks': PairLocks.get_all_locks(),
'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']),
'backtest_start_time': int(backtest_start_time.timestamp()),
'backtest_end_time': int(backtest_end_time.timestamp()),
}
@ -443,16 +478,14 @@ class Backtesting:
data, timerange = self.load_bt_data()
min_date = None
max_date = None
for strat in self.strategylist:
min_date, max_date = self.backtest_one_strategy(strat, data, timerange)
if len(self.strategylist) > 0:
stats = generate_backtest_stats(data, self.all_results,
min_date=min_date, max_date=max_date)
stats = generate_backtest_stats(data, self.all_results,
min_date=min_date, max_date=max_date)
if self.config.get('export', False):
store_backtest_stats(self.config['exportfilename'], stats)
if self.config.get('export', False):
store_backtest_stats(self.config['exportfilename'], stats)
# Show backtest results
show_backtest_results(self.config, stats)
# Show backtest results
show_backtest_results(self.config, stats)

View File

@ -4,36 +4,31 @@
This module contains the hyperopt logic
"""
import io
import locale
import logging
import random
import warnings
from collections import OrderedDict
from datetime import datetime
from math import ceil
from operator import itemgetter
from pathlib import Path
from pprint import pformat
from typing import Any, Dict, List, Optional
import progressbar
import rapidjson
import tabulate
from colorama import Fore, Style
from colorama import init as colorama_init
from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
from pandas import DataFrame, isna, json_normalize
from pandas import DataFrame
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
from freqtrade.data.converter import trim_dataframe
from freqtrade.data.history import get_timerange
from freqtrade.exceptions import OperationalException
from freqtrade.misc import file_dump_json, plural, round_dict
from freqtrade.misc import file_dump_json, plural
from freqtrade.optimize.backtesting import Backtesting
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
from freqtrade.optimize.hyperopt_tools import HyperoptTools
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver
from freqtrade.strategy import IStrategy
@ -73,12 +68,15 @@ class Hyperopt:
self.backtesting = Backtesting(self.config)
self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
self.custom_hyperopt.__class__.strategy = self.backtesting.strategy
self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config)
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
strategy = str(self.config['strategy'])
self.results_file = (self.config['user_data_dir'] /
'hyperopt_results' / f'hyperopt_results_{time_now}.pickle')
'hyperopt_results' /
f'strategy_{strategy}_hyperopt_results_{time_now}.pickle')
self.data_pickle_file = (self.config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_tickerdata.pkl')
self.total_epochs = config.get('epochs', 0)
@ -166,15 +164,6 @@ class Hyperopt:
file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)},
log=False)
@staticmethod
def _read_results(results_file: Path) -> List:
"""
Read hyperopt results from file
"""
logger.info("Reading epochs from '%s'", results_file)
data = load(results_file)
return data
def _get_params_details(self, params: Dict) -> Dict:
"""
Return the params for each space
@ -197,102 +186,16 @@ class Hyperopt:
return result
@staticmethod
def print_epoch_details(results, total_epochs: int, print_json: bool,
no_header: bool = False, header_str: str = None) -> None:
"""
Display details of the hyperopt result
"""
params = results.get('params_details', {})
# Default header string
if header_str is None:
header_str = "Best result"
if not no_header:
explanation_str = Hyperopt._format_explanation_string(results, total_epochs)
print(f"\n{header_str}:\n\n{explanation_str}\n")
if print_json:
result_dict: Dict = {}
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
Hyperopt._params_update_for_json(result_dict, params, s)
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
else:
Hyperopt._params_pretty_print(params, 'buy', "Buy hyperspace params:")
Hyperopt._params_pretty_print(params, 'sell', "Sell hyperspace params:")
Hyperopt._params_pretty_print(params, 'roi', "ROI table:")
Hyperopt._params_pretty_print(params, 'stoploss', "Stoploss:")
Hyperopt._params_pretty_print(params, 'trailing', "Trailing stop:")
@staticmethod
def _params_update_for_json(result_dict, params, space: str) -> None:
if space in params:
space_params = Hyperopt._space_params(params, space)
if space in ['buy', 'sell']:
result_dict.setdefault('params', {}).update(space_params)
elif space == 'roi':
# TODO: get rid of OrderedDict when support for python 3.6 will be
# dropped (dicts keep the order as the language feature)
# Convert keys in min_roi dict to strings because
# rapidjson cannot dump dicts with integer keys...
# OrderedDict is used to keep the numeric order of the items
# in the dict.
result_dict['minimal_roi'] = OrderedDict(
(str(k), v) for k, v in space_params.items()
)
else: # 'stoploss', 'trailing'
result_dict.update(space_params)
@staticmethod
def _params_pretty_print(params, space: str, header: str) -> None:
if space in params:
space_params = Hyperopt._space_params(params, space, 5)
params_result = f"\n# {header}\n"
if space == 'stoploss':
params_result += f"stoploss = {space_params.get('stoploss')}"
elif space == 'roi':
# TODO: get rid of OrderedDict when support for python 3.6 will be
# dropped (dicts keep the order as the language feature)
minimal_roi_result = rapidjson.dumps(
OrderedDict(
(str(k), v) for k, v in space_params.items()
),
default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
params_result += f"minimal_roi = {minimal_roi_result}"
elif space == 'trailing':
for k, v in space_params.items():
params_result += f'{k} = {v}\n'
else:
params_result += f"{space}_params = {pformat(space_params, indent=4)}"
params_result = params_result.replace("}", "\n}").replace("{", "{\n ")
params_result = params_result.replace("\n", "\n ")
print(params_result)
@staticmethod
def _space_params(params, space: str, r: int = None) -> Dict:
d = params[space]
# Round floats to `r` digits after the decimal point if requested
return round_dict(d, r) if r else d
@staticmethod
def is_best_loss(results, current_best_loss: float) -> bool:
return results['loss'] < current_best_loss
def print_results(self, results) -> None:
"""
Log results if it is better than any previous evaluation
TODO: this should be moved to HyperoptTools too
"""
is_best = results['is_best']
if self.print_all or is_best:
print(
self.get_result_table(
HyperoptTools.get_result_table(
self.config, results, self.total_epochs,
self.print_all, self.print_colorized,
self.hyperopt_table_header
@ -300,164 +203,6 @@ class Hyperopt:
)
self.hyperopt_table_header = 2
@staticmethod
def _format_explanation_string(results, total_epochs) -> str:
return (("*" if results['is_initial_point'] else " ") +
f"{results['current_epoch']:5d}/{total_epochs}: " +
f"{results['results_explanation']} " +
f"Objective: {results['loss']:.5f}")
@staticmethod
def get_result_table(config: dict, results: list, total_epochs: int, highlight_best: bool,
print_colorized: bool, remove_header: int) -> str:
"""
Log result table
"""
if not results:
return ''
tabulate.PRESERVE_WHITESPACE = True
trials = json_normalize(results, max_level=1)
trials['Best'] = ''
if 'results_metrics.winsdrawslosses' not in trials.columns:
# Ensure compatibility with older versions of hyperopt results
trials['results_metrics.winsdrawslosses'] = 'N/A'
trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.winsdrawslosses',
'results_metrics.avg_profit', 'results_metrics.total_profit',
'results_metrics.profit', 'results_metrics.duration',
'loss', 'is_initial_point', 'is_best']]
trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit',
'Total profit', 'Profit', 'Avg duration', 'Objective',
'is_initial_point', 'is_best']
trials['is_profit'] = False
trials.loc[trials['is_initial_point'], 'Best'] = '* '
trials.loc[trials['is_best'], 'Best'] = 'Best'
trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best'
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
trials['Trades'] = trials['Trades'].astype(str)
trials['Epoch'] = trials['Epoch'].apply(
lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs)
)
trials['Avg profit'] = trials['Avg profit'].apply(
lambda x: '{:,.2f}%'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
)
trials['Avg duration'] = trials['Avg duration'].apply(
lambda x: '{:,.1f} m'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
)
trials['Objective'] = trials['Objective'].apply(
lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ')
)
trials['Profit'] = trials.apply(
lambda x: '{:,.8f} {} {}'.format(
x['Total profit'], config['stake_currency'],
'({:,.2f}%)'.format(x['Profit']).rjust(10, ' ')
).rjust(25+len(config['stake_currency']))
if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])),
axis=1
)
trials = trials.drop(columns=['Total profit'])
if print_colorized:
for i in range(len(trials)):
if trials.loc[i]['is_profit']:
for j in range(len(trials.loc[i])-3):
trials.iat[i, j] = "{}{}{}".format(Fore.GREEN,
str(trials.loc[i][j]), Fore.RESET)
if trials.loc[i]['is_best'] and highlight_best:
for j in range(len(trials.loc[i])-3):
trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT,
str(trials.loc[i][j]), Style.RESET_ALL)
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit'])
if remove_header > 0:
table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='orgtbl',
headers='keys', stralign="right"
)
table = table.split("\n", remove_header)[remove_header]
elif remove_header < 0:
table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='psql',
headers='keys', stralign="right"
)
table = "\n".join(table.split("\n")[0:remove_header])
else:
table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='psql',
headers='keys', stralign="right"
)
return table
@staticmethod
def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool,
csv_file: str) -> None:
"""
Log result to csv-file
"""
if not results:
return
# Verification for overwrite
if Path(csv_file).is_file():
logger.error(f"CSV file already exists: {csv_file}")
return
try:
io.open(csv_file, 'w+').close()
except IOError:
logger.error(f"Failed to create CSV file: {csv_file}")
return
trials = json_normalize(results, max_level=1)
trials['Best'] = ''
trials['Stake currency'] = config['stake_currency']
base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.avg_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()]
trials = trials[base_metrics + param_metrics]
base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Total profit', 'Stake currency',
'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best']
param_columns = list(results[0]['params_dict'].keys())
trials.columns = base_columns + param_columns
trials['is_profit'] = False
trials.loc[trials['is_initial_point'], 'Best'] = '*'
trials.loc[trials['is_best'], 'Best'] = 'Best'
trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best'
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
trials['Epoch'] = trials['Epoch'].astype(str)
trials['Trades'] = trials['Trades'].astype(str)
trials['Total profit'] = trials['Total profit'].apply(
lambda x: '{:,.8f}'.format(x) if x != 0.0 else ""
)
trials['Profit'] = trials['Profit'].apply(
lambda x: '{:,.2f}'.format(x) if not isna(x) else ""
)
trials['Avg profit'] = trials['Avg profit'].apply(
lambda x: '{:,.2f}%'.format(x) if not isna(x) else ""
)
trials['Avg duration'] = trials['Avg duration'].apply(
lambda x: '{:,.1f} m'.format(x) if not isna(x) else ""
)
trials['Objective'] = trials['Objective'].apply(
lambda x: '{:,.5f}'.format(x) if x != 100000 else ""
)
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit'])
trials.to_csv(csv_file, index=False, header=True, mode='w', encoding='UTF-8')
logger.info(f"CSV file created: {csv_file}")
def has_space(self, space: str) -> bool:
"""
Tell if the space value is contained in the configuration
@ -537,7 +282,6 @@ class Hyperopt:
backtesting_results = self.backtesting.backtest(
processed=processed,
stake_amount=self.config['stake_amount'],
start_date=min_date.datetime,
end_date=max_date.datetime,
max_open_trades=self.max_open_trades,
@ -622,22 +366,6 @@ class Hyperopt:
return parallel(delayed(
wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked)
@staticmethod
def load_previous_results(results_file: Path) -> List:
"""
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:
epochs = Hyperopt._read_results(results_file)
# Detection of some old format, without 'is_best' field saved
if epochs[0].get('is_best') is None:
raise OperationalException(
"The file with Hyperopt results is incompatible with this version "
"of Freqtrade and cannot be loaded.")
logger.info(f"Loaded {len(epochs)} previous evaluations from disk.")
return epochs
def _set_random_state(self, random_state: Optional[int]) -> int:
return random_state or random.randint(1, 2**16 - 1)
@ -661,7 +389,10 @@ class Hyperopt:
dump(preprocessed, self.data_pickle_file)
# We don't need exchange instance anymore while running hyperopt
self.backtesting.exchange = None # type: ignore
self.backtesting.exchange.close()
self.backtesting.exchange._api = None # type: ignore
self.backtesting.exchange._api_async = None # type: ignore
# self.backtesting.exchange = None # type: ignore
self.backtesting.pairlists = None # type: ignore
self.backtesting.strategy.dp = None # type: ignore
IStrategy.dp = None # type: ignore
@ -727,7 +458,7 @@ class Hyperopt:
logger.debug(f"Optimizer epoch evaluated: {val}")
is_best = self.is_best_loss(val, self.current_best_loss)
is_best = HyperoptTools.is_best_loss(val, self.current_best_loss)
# This value is assigned here and not in the optimization method
# to keep proper order in the list of results. That's because
# evaluations can take different time. Here they are aligned in the
@ -755,7 +486,7 @@ class Hyperopt:
if self.epochs:
sorted_epochs = sorted(self.epochs, key=itemgetter('loss'))
best_epoch = sorted_epochs[0]
self.print_epoch_details(best_epoch, self.total_epochs, self.print_json)
HyperoptTools.print_epoch_details(best_epoch, self.total_epochs, self.print_json)
else:
# This is printed when Ctrl+C is pressed quickly, before first epochs have
# a chance to be evaluated.

View File

@ -12,6 +12,7 @@ from skopt.space import Categorical, Dimension, Integer, Real
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import round_dict
from freqtrade.strategy import IStrategy
logger = logging.getLogger(__name__)
@ -34,6 +35,7 @@ class IHyperOpt(ABC):
"""
ticker_interval: str # DEPRECATED
timeframe: str
strategy: IStrategy
def __init__(self, config: dict) -> None:
self.config = config

View File

@ -0,0 +1,294 @@
import io
import logging
from collections import OrderedDict
from pathlib import Path
from pprint import pformat
from typing import Dict, List
import rapidjson
import tabulate
from colorama import Fore, Style
from joblib import load
from pandas import isna, json_normalize
from freqtrade.exceptions import OperationalException
from freqtrade.misc import round_dict
logger = logging.getLogger(__name__)
class HyperoptTools():
@staticmethod
def _read_results(results_file: Path) -> List:
"""
Read hyperopt results from file
"""
logger.info("Reading epochs from '%s'", results_file)
data = load(results_file)
return data
@staticmethod
def load_previous_results(results_file: Path) -> List:
"""
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:
epochs = HyperoptTools._read_results(results_file)
# Detection of some old format, without 'is_best' field saved
if epochs[0].get('is_best') is None:
raise OperationalException(
"The file with HyperoptTools results is incompatible with this version "
"of Freqtrade and cannot be loaded.")
logger.info(f"Loaded {len(epochs)} previous evaluations from disk.")
return epochs
@staticmethod
def print_epoch_details(results, total_epochs: int, print_json: bool,
no_header: bool = False, header_str: str = None) -> None:
"""
Display details of the hyperopt result
"""
params = results.get('params_details', {})
# Default header string
if header_str is None:
header_str = "Best result"
if not no_header:
explanation_str = HyperoptTools._format_explanation_string(results, total_epochs)
print(f"\n{header_str}:\n\n{explanation_str}\n")
if print_json:
result_dict: Dict = {}
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
HyperoptTools._params_update_for_json(result_dict, params, s)
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
else:
HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:")
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:")
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:")
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:")
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:")
@staticmethod
def _params_update_for_json(result_dict, params, space: str) -> None:
if space in params:
space_params = HyperoptTools._space_params(params, space)
if space in ['buy', 'sell']:
result_dict.setdefault('params', {}).update(space_params)
elif space == 'roi':
# TODO: get rid of OrderedDict when support for python 3.6 will be
# dropped (dicts keep the order as the language feature)
# Convert keys in min_roi dict to strings because
# rapidjson cannot dump dicts with integer keys...
# OrderedDict is used to keep the numeric order of the items
# in the dict.
result_dict['minimal_roi'] = OrderedDict(
(str(k), v) for k, v in space_params.items()
)
else: # 'stoploss', 'trailing'
result_dict.update(space_params)
@staticmethod
def _params_pretty_print(params, space: str, header: str) -> None:
if space in params:
space_params = HyperoptTools._space_params(params, space, 5)
params_result = f"\n# {header}\n"
if space == 'stoploss':
params_result += f"stoploss = {space_params.get('stoploss')}"
elif space == 'roi':
# TODO: get rid of OrderedDict when support for python 3.6 will be
# dropped (dicts keep the order as the language feature)
minimal_roi_result = rapidjson.dumps(
OrderedDict(
(str(k), v) for k, v in space_params.items()
),
default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
params_result += f"minimal_roi = {minimal_roi_result}"
elif space == 'trailing':
for k, v in space_params.items():
params_result += f'{k} = {v}\n'
else:
params_result += f"{space}_params = {pformat(space_params, indent=4)}"
params_result = params_result.replace("}", "\n}").replace("{", "{\n ")
params_result = params_result.replace("\n", "\n ")
print(params_result)
@staticmethod
def _space_params(params, space: str, r: int = None) -> Dict:
d = params[space]
# Round floats to `r` digits after the decimal point if requested
return round_dict(d, r) if r else d
@staticmethod
def is_best_loss(results, current_best_loss: float) -> bool:
return results['loss'] < current_best_loss
@staticmethod
def _format_explanation_string(results, total_epochs) -> str:
return (("*" if results['is_initial_point'] else " ") +
f"{results['current_epoch']:5d}/{total_epochs}: " +
f"{results['results_explanation']} " +
f"Objective: {results['loss']:.5f}")
@staticmethod
def get_result_table(config: dict, results: list, total_epochs: int, highlight_best: bool,
print_colorized: bool, remove_header: int) -> str:
"""
Log result table
"""
if not results:
return ''
tabulate.PRESERVE_WHITESPACE = True
trials = json_normalize(results, max_level=1)
trials['Best'] = ''
if 'results_metrics.winsdrawslosses' not in trials.columns:
# Ensure compatibility with older versions of hyperopt results
trials['results_metrics.winsdrawslosses'] = 'N/A'
trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.winsdrawslosses',
'results_metrics.avg_profit', 'results_metrics.total_profit',
'results_metrics.profit', 'results_metrics.duration',
'loss', 'is_initial_point', 'is_best']]
trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit',
'Total profit', 'Profit', 'Avg duration', 'Objective',
'is_initial_point', 'is_best']
trials['is_profit'] = False
trials.loc[trials['is_initial_point'], 'Best'] = '* '
trials.loc[trials['is_best'], 'Best'] = 'Best'
trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best'
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
trials['Trades'] = trials['Trades'].astype(str)
trials['Epoch'] = trials['Epoch'].apply(
lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs)
)
trials['Avg profit'] = trials['Avg profit'].apply(
lambda x: '{:,.2f}%'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
)
trials['Avg duration'] = trials['Avg duration'].apply(
lambda x: '{:,.1f} m'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
)
trials['Objective'] = trials['Objective'].apply(
lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ')
)
trials['Profit'] = trials.apply(
lambda x: '{:,.8f} {} {}'.format(
x['Total profit'], config['stake_currency'],
'({:,.2f}%)'.format(x['Profit']).rjust(10, ' ')
).rjust(25+len(config['stake_currency']))
if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])),
axis=1
)
trials = trials.drop(columns=['Total profit'])
if print_colorized:
for i in range(len(trials)):
if trials.loc[i]['is_profit']:
for j in range(len(trials.loc[i])-3):
trials.iat[i, j] = "{}{}{}".format(Fore.GREEN,
str(trials.loc[i][j]), Fore.RESET)
if trials.loc[i]['is_best'] and highlight_best:
for j in range(len(trials.loc[i])-3):
trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT,
str(trials.loc[i][j]), Style.RESET_ALL)
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit'])
if remove_header > 0:
table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='orgtbl',
headers='keys', stralign="right"
)
table = table.split("\n", remove_header)[remove_header]
elif remove_header < 0:
table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='psql',
headers='keys', stralign="right"
)
table = "\n".join(table.split("\n")[0:remove_header])
else:
table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='psql',
headers='keys', stralign="right"
)
return table
@staticmethod
def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool,
csv_file: str) -> None:
"""
Log result to csv-file
"""
if not results:
return
# Verification for overwrite
if Path(csv_file).is_file():
logger.error(f"CSV file already exists: {csv_file}")
return
try:
io.open(csv_file, 'w+').close()
except IOError:
logger.error(f"Failed to create CSV file: {csv_file}")
return
trials = json_normalize(results, max_level=1)
trials['Best'] = ''
trials['Stake currency'] = config['stake_currency']
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()]
trials = trials[base_metrics + param_metrics]
base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit',
'Stake currency', 'Profit', 'Avg duration', 'Objective',
'is_initial_point', 'is_best']
param_columns = list(results[0]['params_dict'].keys())
trials.columns = base_columns + param_columns
trials['is_profit'] = False
trials.loc[trials['is_initial_point'], 'Best'] = '*'
trials.loc[trials['is_best'], 'Best'] = 'Best'
trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best'
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
trials['Epoch'] = trials['Epoch'].astype(str)
trials['Trades'] = trials['Trades'].astype(str)
trials['Total profit'] = trials['Total profit'].apply(
lambda x: '{:,.8f}'.format(x) if x != 0.0 else ""
)
trials['Profit'] = trials['Profit'].apply(
lambda x: '{:,.2f}'.format(x) if not isna(x) else ""
)
trials['Avg profit'] = trials['Avg profit'].apply(
lambda x: '{:,.2f}%'.format(x) if not isna(x) else ""
)
trials['Avg duration'] = trials['Avg duration'].apply(
lambda x: '{:,.1f} m'.format(x) if not isna(x) else ""
)
trials['Objective'] = trials['Objective'].apply(
lambda x: '{:,.5f}'.format(x) if x != 100000 else ""
)
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit'])
trials.to_csv(csv_file, index=False, header=True, mode='w', encoding='UTF-8')
logger.info(f"CSV file created: {csv_file}")

View File

@ -8,7 +8,7 @@ from numpy import int64
from pandas import DataFrame
from tabulate import tabulate
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT
from freqtrade.data.btanalysis import (calculate_csum, calculate_market_change,
calculate_max_drawdown)
from freqtrade.misc import decimals_per_coin, file_dump_json, round_coin_value
@ -56,12 +56,13 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]:
'Wins', 'Draws', 'Losses']
def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: str) -> Dict:
def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict:
"""
Generate one result dict, with "first_column" as key.
"""
profit_sum = result['profit_ratio'].sum()
profit_total = profit_sum / max_open_trades
# (end-capital - starting capital) / starting capital
profit_total = result['profit_abs'].sum() / starting_balance
return {
'key': first_column,
@ -88,13 +89,13 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column:
}
def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_trades: int,
def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_balance: int,
results: DataFrame, skip_nan: bool = False) -> List[Dict]:
"""
Generates and returns a list for the given backtest data and the results dataframe
:param data: Dict of <pair: dataframe> containing data that was used during backtesting.
:param stake_currency: stake-currency - used to correctly name headers
:param max_open_trades: Maximum allowed open trades
:param starting_balance: Starting balance
:param results: Dataframe containing the backtest results
:param skip_nan: Print "left open" open trades
:return: List of Dicts containing the metrics per pair
@ -107,10 +108,10 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_t
if skip_nan and result['profit_abs'].isnull().all():
continue
tabular_data.append(_generate_result_line(result, max_open_trades, pair))
tabular_data.append(_generate_result_line(result, starting_balance, pair))
# Append Total
tabular_data.append(_generate_result_line(results, max_open_trades, 'TOTAL'))
tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL'))
return tabular_data
@ -132,7 +133,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List
tabular_data.append(
{
'sell_reason': reason.value,
'sell_reason': reason,
'trades': count,
'wins': len(result[result['profit_abs'] > 0]),
'draws': len(result[result['profit_abs'] == 0]),
@ -159,7 +160,7 @@ def generate_strategy_metrics(all_results: Dict) -> List[Dict]:
tabular_data = []
for strategy, results in all_results.items():
tabular_data.append(_generate_result_line(
results['results'], results['config']['max_open_trades'], strategy)
results['results'], results['config']['dry_run_wallet'], strategy)
)
return tabular_data
@ -195,13 +196,18 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
return {
'backtest_best_day': 0,
'backtest_worst_day': 0,
'backtest_best_day_abs': 0,
'backtest_worst_day_abs': 0,
'winning_days': 0,
'draw_days': 0,
'losing_days': 0,
'winner_holding_avg': timedelta(),
'loser_holding_avg': timedelta(),
}
daily_profit = results.resample('1d', on='close_date')['profit_ratio'].sum()
daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum()
daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10)
worst_rel = min(daily_profit_rel)
best_rel = max(daily_profit_rel)
worst = min(daily_profit)
best = max(daily_profit)
winning_days = sum(daily_profit > 0)
@ -212,8 +218,10 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
losing_trades = results.loc[results['profit_ratio'] < 0]
return {
'backtest_best_day': best,
'backtest_worst_day': worst,
'backtest_best_day': best_rel,
'backtest_worst_day': worst_rel,
'backtest_best_day_abs': best,
'backtest_worst_day_abs': worst,
'winning_days': winning_days,
'draw_days': draw_days,
'losing_days': losing_days,
@ -246,15 +254,16 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
continue
config = content['config']
max_open_trades = min(config['max_open_trades'], len(btdata.keys()))
starting_balance = config['dry_run_wallet']
stake_currency = config['stake_currency']
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
max_open_trades=max_open_trades,
starting_balance=starting_balance,
results=results, skip_nan=False)
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
results=results)
left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
max_open_trades=max_open_trades,
starting_balance=starting_balance,
results=results.loc[results['is_open']],
skip_nan=True)
daily_stats = generate_daily_stats(results)
@ -275,8 +284,10 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
'sell_reason_summary': sell_reason_stats,
'left_open_trades': left_open_results,
'total_trades': len(results),
'total_volume': float(results['stake_amount'].sum()),
'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0,
'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0,
'profit_total': results['profit_ratio'].sum() / max_open_trades,
'profit_total': results['profit_abs'].sum() / starting_balance,
'profit_total_abs': results['profit_abs'].sum(),
'backtest_start': min_date.datetime,
'backtest_start_ts': min_date.int_timestamp * 1000,
@ -292,6 +303,10 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
'pairlist': list(btdata.keys()),
'stake_amount': config['stake_amount'],
'stake_currency': config['stake_currency'],
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
'starting_balance': starting_balance,
'dry_run_wallet': starting_balance,
'final_balance': content['final_balance'],
'max_open_trades': max_open_trades,
'max_open_trades_setting': (config['max_open_trades']
if config['max_open_trades'] != float('inf') else -1),
@ -316,17 +331,23 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
result['strategy'][strategy] = strat_stats
try:
max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown(
max_drawdown, _, _, _, _ = calculate_max_drawdown(
results, value_col='profit_ratio')
drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown(
results, value_col='profit_abs')
strat_stats.update({
'max_drawdown': max_drawdown,
'max_drawdown_abs': drawdown_abs,
'drawdown_start': drawdown_start,
'drawdown_start_ts': drawdown_start.timestamp() * 1000,
'drawdown_end': drawdown_end,
'drawdown_end_ts': drawdown_end.timestamp() * 1000,
'max_drawdown_low': low_val,
'max_drawdown_high': high_val,
})
csum_min, csum_max = calculate_csum(results)
csum_min, csum_max = calculate_csum(results, starting_balance)
strat_stats.update({
'csum_min': csum_min,
'csum_max': csum_max
@ -335,6 +356,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
except ValueError:
strat_stats.update({
'max_drawdown': 0.0,
'max_drawdown_abs': 0.0,
'max_drawdown_low': 0.0,
'max_drawdown_high': 0.0,
'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc),
'drawdown_start_ts': 0,
'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc),
@ -431,8 +455,19 @@ def text_table_add_metrics(strat_results: Dict) -> str:
('Max open trades', strat_results['max_open_trades']),
('', ''), # Empty line to improve readability
('Total trades', strat_results['total_trades']),
('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"),
('Starting balance', round_coin_value(strat_results['starting_balance'],
strat_results['stake_currency'])),
('Final balance', round_coin_value(strat_results['final_balance'],
strat_results['stake_currency'])),
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
strat_results['stake_currency'])),
('Total profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"),
('Trades per day', strat_results['trades_per_day']),
('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'],
strat_results['stake_currency'])),
('Total trade volume', round_coin_value(strat_results['total_volume'],
strat_results['stake_currency'])),
('', ''), # Empty line to improve readability
('Best Pair', f"{strat_results['best_pair']['key']} "
f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"),
@ -442,20 +477,28 @@ def text_table_add_metrics(strat_results: Dict) -> str:
('Worst trade', f"{worst_trade['pair']} "
f"{round(worst_trade['profit_ratio'] * 100, 2)}%"),
('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"),
('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"),
('Best day', round_coin_value(strat_results['backtest_best_day_abs'],
strat_results['stake_currency'])),
('Worst day', round_coin_value(strat_results['backtest_worst_day_abs'],
strat_results['stake_currency'])),
('Days win/draw/lose', f"{strat_results['winning_days']} / "
f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
('', ''), # Empty line to improve readability
('Abs Profit Min', round_coin_value(strat_results['csum_min'],
strat_results['stake_currency'])),
('Abs Profit Max', round_coin_value(strat_results['csum_max'],
strat_results['stake_currency'])),
('Min balance', round_coin_value(strat_results['csum_min'],
strat_results['stake_currency'])),
('Max balance', round_coin_value(strat_results['csum_max'],
strat_results['stake_currency'])),
('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"),
('Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"),
('Drawdown', round_coin_value(strat_results['max_drawdown_abs'],
strat_results['stake_currency'])),
('Drawdown high', round_coin_value(strat_results['max_drawdown_high'],
strat_results['stake_currency'])),
('Drawdown low', round_coin_value(strat_results['max_drawdown_low'],
strat_results['stake_currency'])),
('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)),
('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)),
('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"),
@ -463,7 +506,17 @@ def text_table_add_metrics(strat_results: Dict) -> str:
return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl")
else:
return ''
start_balance = round_coin_value(strat_results['starting_balance'],
strat_results['stake_currency'])
stake_amount = round_coin_value(
strat_results['stake_amount'], strat_results['stake_currency']
) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited'
message = ("No trades made. "
f"Your starting balance was {start_balance}, "
f"and your stake was {stake_amount}."
)
return message
def show_backtest_results(config: Dict, backtest_stats: Dict):

View File

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

View File

@ -141,7 +141,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
inspector = inspect(engine)
cols = inspector.get_columns('trades')
if 'orders' not in previous_tables:
if 'orders' not in previous_tables and 'trades' in previous_tables:
logger.info('Moving open orders to Orders table.')
migrate_open_orders_to_trades(engine)
else:

View File

@ -199,67 +199,69 @@ class Order(_DECL_BASE):
return Order.query.filter(Order.ft_is_open.is_(True)).all()
class Trade(_DECL_BASE):
class LocalTrade():
"""
Trade database model.
Also handles updating and querying trades
Used in backtesting - must be aligned to Trade model!
"""
__tablename__ = 'trades'
use_db: bool = True
use_db: bool = False
# Trades container for backtesting
trades: List['Trade'] = []
trades: List['LocalTrade'] = []
trades_open: List['LocalTrade'] = []
total_profit: float = 0
id = Column(Integer, primary_key=True)
id: int = 0
orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan")
orders: List[Order] = []
exchange = Column(String, nullable=False)
pair = Column(String, nullable=False, index=True)
is_open = Column(Boolean, nullable=False, default=True, index=True)
fee_open = Column(Float, nullable=False, default=0.0)
fee_open_cost = Column(Float, nullable=True)
fee_open_currency = Column(String, nullable=True)
fee_close = Column(Float, nullable=False, default=0.0)
fee_close_cost = Column(Float, nullable=True)
fee_close_currency = Column(String, nullable=True)
open_rate = Column(Float)
open_rate_requested = Column(Float)
exchange: str = ''
pair: str = ''
is_open: bool = True
fee_open: float = 0.0
fee_open_cost: Optional[float] = None
fee_open_currency: str = ''
fee_close: float = 0.0
fee_close_cost: Optional[float] = None
fee_close_currency: str = ''
open_rate: float = 0.0
open_rate_requested: Optional[float] = None
# open_trade_value - calculated via _calc_open_trade_value
open_trade_value = Column(Float)
close_rate = Column(Float)
close_rate_requested = Column(Float)
close_profit = Column(Float)
close_profit_abs = Column(Float)
stake_amount = Column(Float, nullable=False)
amount = Column(Float)
amount_requested = Column(Float)
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
close_date = Column(DateTime)
open_order_id = Column(String)
open_trade_value: float = 0.0
close_rate: Optional[float] = None
close_rate_requested: Optional[float] = None
close_profit: Optional[float] = None
close_profit_abs: Optional[float] = None
stake_amount: float = 0.0
amount: float = 0.0
amount_requested: Optional[float] = None
open_date: datetime
close_date: Optional[datetime] = None
open_order_id: Optional[str] = None
# absolute value of the stop loss
stop_loss = Column(Float, nullable=True, default=0.0)
stop_loss: float = 0.0
# percentage value of the stop loss
stop_loss_pct = Column(Float, nullable=True)
stop_loss_pct: float = 0.0
# absolute value of the initial stop loss
initial_stop_loss = Column(Float, nullable=True, default=0.0)
initial_stop_loss: float = 0.0
# percentage value of the initial stop loss
initial_stop_loss_pct = Column(Float, nullable=True)
initial_stop_loss_pct: float = 0.0
# stoploss order id which is on exchange
stoploss_order_id = Column(String, nullable=True, index=True)
stoploss_order_id: Optional[str] = None
# last update time of the stoploss order on exchange
stoploss_last_update = Column(DateTime, nullable=True)
stoploss_last_update: Optional[datetime] = None
# absolute value of the highest reached price
max_rate = Column(Float, nullable=True, default=0.0)
max_rate: float = 0.0
# Lowest price reached
min_rate = Column(Float, nullable=True)
sell_reason = Column(String, nullable=True)
sell_order_status = Column(String, nullable=True)
strategy = Column(String, nullable=True)
timeframe = Column(Integer, nullable=True)
min_rate: float = 0.0
sell_reason: str = ''
sell_order_status: str = ''
strategy: str = ''
timeframe: Optional[int] = None
def __init__(self, **kwargs):
super().__init__(**kwargs)
for key in kwargs:
setattr(self, key, kwargs[key])
self.recalc_open_trade_value()
def __repr__(self):
@ -268,6 +270,14 @@ class Trade(_DECL_BASE):
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
f'open_rate={self.open_rate:.8f}, open_since={open_since})')
@property
def open_date_utc(self):
return self.open_date.replace(tzinfo=timezone.utc)
@property
def close_date_utc(self):
return self.close_date.replace(tzinfo=timezone.utc)
def to_json(self) -> Dict[str, Any]:
return {
'trade_id': self.id,
@ -306,9 +316,9 @@ class Trade(_DECL_BASE):
'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
'close_profit_abs': self.close_profit_abs, # Deprecated
'trade_duration_s': (int((self.close_date - self.open_date).total_seconds())
'trade_duration_s': (int((self.close_date_utc - self.open_date_utc).total_seconds())
if self.close_date else None),
'trade_duration': (int((self.close_date - self.open_date).total_seconds() // 60)
'trade_duration': (int((self.close_date_utc - self.open_date_utc).total_seconds() // 60)
if self.close_date else None),
'profit_ratio': self.close_profit,
@ -341,8 +351,9 @@ class Trade(_DECL_BASE):
"""
Resets all trades. Only active for backtesting mode.
"""
if not Trade.use_db:
Trade.trades = []
LocalTrade.trades = []
LocalTrade.trades_open = []
LocalTrade.total_profit = 0
def adjust_min_max_rates(self, current_price: float) -> None:
"""
@ -410,8 +421,8 @@ class Trade(_DECL_BASE):
if order_type in ('market', 'limit') and order['side'] == 'buy':
# Update open rate and actual amount
self.open_rate = Decimal(safe_value_fallback(order, 'average', 'price'))
self.amount = Decimal(safe_value_fallback(order, 'filled', 'amount'))
self.open_rate = float(safe_value_fallback(order, 'average', 'price'))
self.amount = float(safe_value_fallback(order, 'filled', 'amount'))
self.recalc_open_trade_value()
if self.is_open:
logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.')
@ -425,7 +436,7 @@ class Trade(_DECL_BASE):
self.close_rate_requested = self.stop_loss
if self.is_open:
logger.info(f'{order_type.upper()} is hit for {self}.')
self.close(order['average'])
self.close(safe_value_fallback(order, 'average', 'price'))
else:
raise ValueError(f'Unknown order type: {order_type}')
cleanup_db()
@ -435,7 +446,7 @@ class Trade(_DECL_BASE):
Sets close_rate to the given rate, calculates total profit
and marks trade as closed
"""
self.close_rate = Decimal(rate)
self.close_rate = rate
self.close_profit = self.calc_profit_ratio()
self.close_profit_abs = self.calc_profit()
self.close_date = self.close_date or datetime.utcnow()
@ -480,14 +491,6 @@ class Trade(_DECL_BASE):
def update_order(self, order: Dict) -> None:
Order.update_orders(self.orders, order)
def delete(self) -> None:
for order in self.orders:
Order.session.delete(order)
Trade.session.delete(self)
Trade.session.flush()
def _calc_open_trade_value(self) -> float:
"""
Calculate the open_rate including open_fee.
@ -517,7 +520,7 @@ class Trade(_DECL_BASE):
if rate is None and not self.close_rate:
return 0.0
sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate)
sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore
fees = sell_trade * Decimal(fee or self.fee_close)
return float(sell_trade - fees)
@ -589,7 +592,7 @@ class Trade(_DECL_BASE):
@staticmethod
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
open_date: datetime = None, close_date: datetime = None,
) -> List['Trade']:
) -> List['LocalTrade']:
"""
Helper function to query Trades.
Returns a List of trades, filtered on the parameters given.
@ -598,30 +601,40 @@ class Trade(_DECL_BASE):
:return: unsorted List[Trade]
"""
if Trade.use_db:
trade_filter = []
if pair:
trade_filter.append(Trade.pair == pair)
if open_date:
trade_filter.append(Trade.open_date > open_date)
if close_date:
trade_filter.append(Trade.close_date > close_date)
if is_open is not None:
trade_filter.append(Trade.is_open.is_(is_open))
return Trade.get_trades(trade_filter).all()
# Offline mode - without database
if is_open is not None:
if is_open:
sel_trades = LocalTrade.trades_open
else:
sel_trades = LocalTrade.trades
else:
# Offline mode - without database
sel_trades = [trade for trade in Trade.trades]
if pair:
sel_trades = [trade for trade in sel_trades if trade.pair == pair]
if open_date:
sel_trades = [trade for trade in sel_trades if trade.open_date > open_date]
if close_date:
sel_trades = [trade for trade in sel_trades if trade.close_date
and trade.close_date > close_date]
if is_open is not None:
sel_trades = [trade for trade in sel_trades if trade.is_open == is_open]
return sel_trades
# Not used during backtesting, but might be used by a strategy
sel_trades = [trade for trade in LocalTrade.trades + LocalTrade.trades_open]
if pair:
sel_trades = [trade for trade in sel_trades if trade.pair == pair]
if open_date:
sel_trades = [trade for trade in sel_trades if trade.open_date > open_date]
if close_date:
sel_trades = [trade for trade in sel_trades if trade.close_date
and trade.close_date > close_date]
return sel_trades
@staticmethod
def close_bt_trade(trade):
LocalTrade.trades_open.remove(trade)
LocalTrade.trades.append(trade)
LocalTrade.total_profit += trade.close_profit_abs
@staticmethod
def add_bt_trade(trade):
if trade.is_open:
LocalTrade.trades_open.append(trade)
else:
LocalTrade.trades.append(trade)
@staticmethod
def get_open_trades() -> List[Any]:
@ -663,9 +676,12 @@ class Trade(_DECL_BASE):
Calculates total invested amount in open trades
in stake currency
"""
total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount))\
.filter(Trade.is_open.is_(True))\
.scalar()
if Trade.use_db:
total_open_stake_amount = Trade.session.query(
func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar()
else:
total_open_stake_amount = sum(
t.stake_amount for t in Trade.get_trades_proxy(is_open=True))
return total_open_stake_amount or 0
@staticmethod
@ -723,6 +739,108 @@ class Trade(_DECL_BASE):
logger.info(f"New stoploss: {trade.stop_loss}.")
class Trade(_DECL_BASE, LocalTrade):
"""
Trade database model.
Also handles updating and querying trades
Note: Fields must be aligned with LocalTrade class
"""
__tablename__ = 'trades'
use_db: bool = True
id = Column(Integer, primary_key=True)
orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan")
exchange = Column(String, nullable=False)
pair = Column(String, nullable=False, index=True)
is_open = Column(Boolean, nullable=False, default=True, index=True)
fee_open = Column(Float, nullable=False, default=0.0)
fee_open_cost = Column(Float, nullable=True)
fee_open_currency = Column(String, nullable=True)
fee_close = Column(Float, nullable=False, default=0.0)
fee_close_cost = Column(Float, nullable=True)
fee_close_currency = Column(String, nullable=True)
open_rate = Column(Float)
open_rate_requested = Column(Float)
# open_trade_value - calculated via _calc_open_trade_value
open_trade_value = Column(Float)
close_rate = Column(Float)
close_rate_requested = Column(Float)
close_profit = Column(Float)
close_profit_abs = Column(Float)
stake_amount = Column(Float, nullable=False)
amount = Column(Float)
amount_requested = Column(Float)
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
close_date = Column(DateTime)
open_order_id = Column(String)
# absolute value of the stop loss
stop_loss = Column(Float, nullable=True, default=0.0)
# percentage value of the stop loss
stop_loss_pct = Column(Float, nullable=True)
# absolute value of the initial stop loss
initial_stop_loss = Column(Float, nullable=True, default=0.0)
# percentage value of the initial stop loss
initial_stop_loss_pct = Column(Float, nullable=True)
# stoploss order id which is on exchange
stoploss_order_id = Column(String, nullable=True, index=True)
# last update time of the stoploss order on exchange
stoploss_last_update = Column(DateTime, nullable=True)
# absolute value of the highest reached price
max_rate = Column(Float, nullable=True, default=0.0)
# Lowest price reached
min_rate = Column(Float, nullable=True)
sell_reason = Column(String, nullable=True)
sell_order_status = Column(String, nullable=True)
strategy = Column(String, nullable=True)
timeframe = Column(Integer, nullable=True)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.recalc_open_trade_value()
def delete(self) -> None:
for order in self.orders:
Order.session.delete(order)
Trade.session.delete(self)
Trade.session.flush()
@staticmethod
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
open_date: datetime = None, close_date: datetime = None,
) -> List['LocalTrade']:
"""
Helper function to query Trades.
Returns a List of trades, filtered on the parameters given.
In live mode, converts the filter to a database query and returns all rows
In Backtest mode, uses filters on Trade.trades to get the result.
:return: unsorted List[Trade]
"""
if Trade.use_db:
trade_filter = []
if pair:
trade_filter.append(Trade.pair == pair)
if open_date:
trade_filter.append(Trade.open_date > open_date)
if close_date:
trade_filter.append(Trade.close_date > close_date)
if is_open is not None:
trade_filter.append(Trade.is_open.is_(is_open))
return Trade.get_trades(trade_filter).all()
else:
return LocalTrade.get_trades_proxy(
pair=pair, is_open=is_open,
open_date=open_date,
close_date=close_date
)
class PairLock(_DECL_BASE):
"""
Pair Locks database model.
@ -765,6 +883,7 @@ class PairLock(_DECL_BASE):
def to_json(self) -> Dict[str, Any]:
return {
'id': self.id,
'pair': self.pair,
'lock_time': self.lock_time.strftime(DATETIME_PRINT_FORMAT),
'lock_timestamp': int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000),

View File

@ -123,3 +123,11 @@ class PairLocks():
now = datetime.now(timezone.utc)
return len(PairLocks.get_pair_locks(pair, now)) > 0 or PairLocks.is_global_lock(now)
@staticmethod
def get_all_locks() -> List[PairLock]:
if PairLocks.use_db:
return PairLock.query.all()
else:
return PairLocks.locks

View File

@ -145,7 +145,7 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame,
Add scatter points indicating max drawdown
"""
try:
max_drawdown, highdate, lowdate = calculate_max_drawdown(trades)
max_drawdown, highdate, lowdate, _, _ = calculate_max_drawdown(trades)
drawdown = go.Scatter(
x=[highdate, lowdate],

View File

@ -64,7 +64,7 @@ class PriceFilter(IPairList):
:param ticker: ticker dict as returned from ccxt.load_markets()
:return: True if the pair can stay, false if it should be removed
"""
if ticker['last'] is None or ticker['last'] == 0:
if ticker.get('last', None) is None or ticker.get('last') == 0:
self.log_once(f"Removed {pair} from whitelist, because "
"ticker['last'] is empty (Usually no trade in the last 24h).",
logger.info)

View File

@ -28,7 +28,7 @@ class RangeStabilityFilter(IPairList):
self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01)
self._refresh_period = pairlistconfig.get('refresh_period', 1440)
self._pair_cache: TTLCache = TTLCache(maxsize=100, ttl=self._refresh_period)
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
if self._days < 1:
raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")

View File

@ -44,7 +44,8 @@ class CooldownPeriod(IProtection):
trades = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
if trades:
# Get latest trade
trade = sorted(trades, key=lambda t: t.close_date)[-1]
# Ignore type error as we know we only get closed trades.
trade = sorted(trades, key=lambda t: t.close_date)[-1] # type: ignore
self.log_once(f"Cooldown for {pair} for {self.stop_duration_str}.", logger.info)
until = self.calculate_lock_end([trade], self._stop_duration)

View File

@ -7,7 +7,7 @@ from typing import Any, Dict, List, Optional, Tuple
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import plural
from freqtrade.mixins import LoggingMixin
from freqtrade.persistence import Trade
from freqtrade.persistence import LocalTrade
logger = logging.getLogger(__name__)
@ -93,11 +93,11 @@ class IProtection(LoggingMixin, ABC):
"""
@staticmethod
def calculate_lock_end(trades: List[Trade], stop_minutes: int) -> datetime:
def calculate_lock_end(trades: List[LocalTrade], stop_minutes: int) -> datetime:
"""
Get lock end time
"""
max_date: datetime = max([trade.close_date for trade in trades])
max_date: datetime = max([trade.close_date for trade in trades if trade.close_date])
# comming from Database, tzinfo is not set.
if max_date.tzinfo is None:
max_date = max_date.replace(tzinfo=timezone.utc)

View File

@ -53,7 +53,7 @@ class LowProfitPairs(IProtection):
# Not enough trades in the relevant period
return False, None, None
profit = sum(trade.close_profit for trade in trades)
profit = sum(trade.close_profit for trade in trades if trade.close_profit)
if profit < self._required_profit:
self.log_once(
f"Trading for {pair} stopped due to {profit:.2f} < {self._required_profit} "

View File

@ -55,7 +55,7 @@ class MaxDrawdown(IProtection):
# Drawdown is always positive
try:
drawdown, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit')
drawdown, _, _, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit')
except ValueError:
return False, None, None

View File

@ -56,7 +56,7 @@ class StoplossGuard(IProtection):
trades = [trade for trade in trades1 if (str(trade.sell_reason) in (
SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value,
SellType.STOPLOSS_ON_EXCHANGE.value)
and trade.close_profit < 0)]
and trade.close_profit and trade.close_profit < 0)]
if len(trades) < self._trade_limit:
return False, None, None

View File

@ -1,5 +1,5 @@
from datetime import date, datetime
from typing import Any, Dict, List, Optional, TypeVar, Union
from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel
@ -62,14 +62,12 @@ class PerformanceEntry(BaseModel):
class Profit(BaseModel):
profit_closed_coin: float
profit_closed_percent: float
profit_closed_percent_mean: float
profit_closed_ratio_mean: float
profit_closed_percent_sum: float
profit_closed_ratio_sum: float
profit_closed_fiat: float
profit_all_coin: float
profit_all_percent: float
profit_all_percent_mean: float
profit_all_ratio_mean: float
profit_all_percent_sum: float
@ -205,10 +203,12 @@ class TradeResponse(BaseModel):
trades_count: int
ForceBuyResponse = TypeVar('ForceBuyResponse', TradeSchema, StatusMsg)
class ForceBuyResponse(BaseModel):
__root__: Union[TradeSchema, StatusMsg]
class LockModel(BaseModel):
id: int
active: bool
lock_end_time: str
lock_end_timestamp: int
@ -223,6 +223,11 @@ class Locks(BaseModel):
locks: List[LockModel]
class DeleteLockRequest(BaseModel):
pair: Optional[str]
lockid: Optional[int]
class Logs(BaseModel):
log_count: int
logs: List[List]
@ -267,7 +272,8 @@ class PlotConfig_(BaseModel):
subplots: Optional[Dict[str, Any]]
PlotConfig = TypeVar('PlotConfig', PlotConfig_, Dict)
class PlotConfig(BaseModel):
__root__: Union[PlotConfig_, Dict]
class StrategyListResponse(BaseModel):

View File

@ -11,13 +11,14 @@ from freqtrade.data.history import get_datahandler
from freqtrade.exceptions import OperationalException
from freqtrade.rpc import RPC
from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload,
BlacklistResponse, Count, Daily, DeleteTrade,
ForceBuyPayload, ForceBuyResponse,
ForceSellPayload, Locks, Logs, OpenTradeSchema,
PairHistory, PerformanceEntry, Ping, PlotConfig,
Profit, ResultMsg, ShowConfig, Stats, StatusMsg,
StrategyListResponse, StrategyResponse,
TradeResponse, Version, WhitelistResponse)
BlacklistResponse, Count, Daily,
DeleteLockRequest, DeleteTrade, ForceBuyPayload,
ForceBuyResponse, ForceSellPayload, Locks, Logs,
OpenTradeSchema, PairHistory, PerformanceEntry,
Ping, PlotConfig, Profit, ResultMsg, ShowConfig,
Stats, StatusMsg, StrategyListResponse,
StrategyResponse, TradeResponse, Version,
WhitelistResponse)
from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional
from freqtrade.rpc.rpc import RPCException
@ -111,9 +112,9 @@ def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
trade = rpc._rpc_forcebuy(payload.pair, payload.price)
if trade:
return trade.to_json()
return ForceBuyResponse.parse_obj(trade.to_json())
else:
return {"status": f"Error buying pair {payload.pair}."}
return ForceBuyResponse.parse_obj({"status": f"Error buying pair {payload.pair}."})
@router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
@ -136,11 +137,21 @@ def whitelist(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_whitelist()
@router.get('/locks', response_model=Locks, tags=['info'])
@router.get('/locks', response_model=Locks, tags=['info', 'locks'])
def locks(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_locks()
@router.delete('/locks/{lockid}', response_model=Locks, tags=['info', 'locks'])
def delete_lock(lockid: int, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_delete_lock(lockid=lockid)
@router.post('/locks/delete', response_model=Locks, tags=['info', 'locks'])
def delete_lock_pair(payload: DeleteLockRequest, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_delete_lock(lockid=payload.lockid, pair=payload.pair)
@router.get('/logs', response_model=Logs, tags=['info'])
def logs(limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_get_logs(limit)
@ -183,7 +194,7 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str,
@router.get('/plot_config', response_model=PlotConfig, tags=['candle data'])
def plot_config(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_plot_config()
return PlotConfig.parse_obj(rpc._rpc_plot_config())
@router.get('/strategies', response_model=StrategyListResponse, tags=['strategy'])

View File

@ -8,12 +8,33 @@ import uvicorn
class UvicornServer(uvicorn.Server):
"""
Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742
Removed install_signal_handlers() override based on changes from this commit:
https://github.com/encode/uvicorn/commit/ce2ef45a9109df8eae038c0ec323eb63d644cbc6
Cannot rely on asyncio.get_event_loop() to create new event loop because of this check:
https://github.com/python/cpython/blob/4d7f11e05731f67fd2c07ec2972c6cb9861d52be/Lib/asyncio/events.py#L638
Fix by overriding run() and forcing creation of new event loop if uvloop is available
"""
def install_signal_handlers(self):
def run(self, sockets=None):
import asyncio
"""
In the parent implementation, this starts the thread, therefore we must patch it away here.
Parent implementation calls self.config.setup_event_loop(),
but we need to create uvloop event loop manually
"""
pass
try:
import uvloop # noqa
except ImportError: # pragma: no cover
from uvicorn.loops.asyncio import asyncio_setup
asyncio_setup()
else:
asyncio.set_event_loop(uvloop.new_event_loop())
loop = asyncio.get_event_loop()
loop.run_until_complete(self.serve(sockets=sockets))
@contextlib.contextmanager
def run_in_thread(self):

View File

@ -10,7 +10,7 @@ router_ui = APIRouter()
@router_ui.get('/favicon.ico', include_in_schema=False)
async def favicon():
return FileResponse(Path(__file__).parent / 'ui/favicon.ico')
return FileResponse(str(Path(__file__).parent / 'ui/favicon.ico'))
@router_ui.get('/{rest_of_path:path}', include_in_schema=False)

View File

@ -3,7 +3,7 @@ This module contains class to define a RPC communications
"""
import logging
from abc import abstractmethod
from datetime import date, datetime, timedelta
from datetime import date, datetime, timedelta, timezone
from enum import Enum
from math import isnan
from typing import Any, Dict, List, Optional, Tuple, Union
@ -20,6 +20,7 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
from freqtrade.loggers import bufferHandler
from freqtrade.misc import shorten_date
from freqtrade.persistence import PairLocks, Trade
from freqtrade.persistence.models import PairLock
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.state import State
@ -288,9 +289,10 @@ class RPC:
""" Returns the X last trades """
if limit > 0:
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
Trade.id.desc()).limit(limit)
Trade.close_date.desc()).limit(limit)
else:
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(Trade.id.desc()).all()
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
Trade.close_date.desc()).all()
output = [trade.to_json() for trade in trades]
@ -401,14 +403,12 @@ class RPC:
num = float(len(durations) or 1)
return {
'profit_closed_coin': profit_closed_coin_sum,
'profit_closed_percent': round(profit_closed_ratio_mean * 100, 2), # DEPRECATED
'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2),
'profit_closed_ratio_mean': profit_closed_ratio_mean,
'profit_closed_percent_sum': round(profit_closed_ratio_sum * 100, 2),
'profit_closed_ratio_sum': profit_closed_ratio_sum,
'profit_closed_fiat': profit_closed_fiat,
'profit_all_coin': profit_all_coin_sum,
'profit_all_percent': round(profit_all_ratio_mean * 100, 2), # DEPRECATED
'profit_all_percent_mean': round(profit_all_ratio_mean * 100, 2),
'profit_all_ratio_mean': profit_all_ratio_mean,
'profit_all_percent_sum': round(profit_all_ratio_sum * 100, 2),
@ -594,7 +594,7 @@ class RPC:
pair, self._freqtrade.get_free_open_trades())
# execute buy
if self._freqtrade.execute_buy(pair, stakeamount, price):
if self._freqtrade.execute_buy(pair, stakeamount, price, forcebuy=True):
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
return trade
else:
@ -663,7 +663,7 @@ class RPC:
}
def _rpc_locks(self) -> Dict[str, Any]:
""" Returns the current locks"""
""" Returns the current locks """
locks = PairLocks.get_pair_locks(None)
return {
@ -671,6 +671,25 @@ class RPC:
'locks': [lock.to_json() for lock in locks]
}
def _rpc_delete_lock(self, lockid: Optional[int] = None,
pair: Optional[str] = None) -> Dict[str, Any]:
""" Delete specific lock(s) """
locks = []
if pair:
locks = PairLocks.get_pair_locks(pair)
if lockid:
locks = PairLock.query.filter(PairLock.id == lockid).all()
for lock in locks:
lock.active = False
lock.lock_end_time = datetime.now(timezone.utc)
# session is always the same
PairLock.session.flush()
return self._rpc_locks()
def _rpc_whitelist(self) -> Dict:
""" Returns the currently active whitelist"""
res = {'method': self._freqtrade.pairlists.name_list,

View File

@ -6,6 +6,7 @@ This module manage Telegram communication
import json
import logging
from datetime import timedelta
from html import escape
from itertools import chain
from typing import Any, Callable, Dict, List, Union
@ -17,6 +18,7 @@ from telegram.ext import CallbackContext, CommandHandler, Updater
from telegram.utils.helpers import escape_markdown
from freqtrade.__init__ import __version__
from freqtrade.constants import DUST_PER_COIN
from freqtrade.exceptions import OperationalException
from freqtrade.misc import round_coin_value
from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType
@ -143,6 +145,7 @@ class Telegram(RPCHandler):
CommandHandler('daily', self._daily),
CommandHandler('count', self._count),
CommandHandler('locks', self._locks),
CommandHandler(['unlock', 'delete_locks'], self._delete_locks),
CommandHandler(['reload_config', 'reload_conf'], self._reload_config),
CommandHandler(['show_config', 'show_conf'], self._show_config),
CommandHandler('stopbuy', self._stopbuy),
@ -190,7 +193,8 @@ class Telegram(RPCHandler):
else:
msg['stake_amount_fiat'] = 0
message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}\n"
message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}"
f" (#{msg['trade_id']})\n"
f"*Amount:* `{msg['amount']:.8f}`\n"
f"*Open Rate:* `{msg['limit']:.8f}`\n"
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
@ -202,7 +206,8 @@ class Telegram(RPCHandler):
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
message = ("\N{WARNING SIGN} *{exchange}:* "
"Cancelling open buy Order for {pair}. Reason: {reason}.".format(**msg))
"Cancelling open buy Order for {pair} (#{trade_id}). "
"Reason: {reason}.".format(**msg))
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
msg['amount'] = round(msg['amount'], 8)
@ -213,7 +218,7 @@ class Telegram(RPCHandler):
msg['emoji'] = self._get_sell_emoji(msg)
message = ("{emoji} *{exchange}:* Selling {pair}\n"
message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n"
"*Amount:* `{amount:.8f}`\n"
"*Open Rate:* `{open_rate:.8f}`\n"
"*Current Rate:* `{current_rate:.8f}`\n"
@ -233,7 +238,7 @@ class Telegram(RPCHandler):
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order "
"for {pair}. Reason: {reason}").format(**msg)
"for {pair} (#{trade_id}). Reason: {reason}").format(**msg)
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
message = '*Status:* `{status}`'.format(**msg)
@ -338,8 +343,17 @@ class Telegram(RPCHandler):
statlist, head = self._rpc._rpc_status_table(
self._config['stake_currency'], self._config.get('fiat_display_currency', ''))
message = tabulate(statlist, headers=head, tablefmt='simple')
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
max_trades_per_msg = 50
"""
Calculate the number of messages of 50 trades per message
0.99 is used to make sure that there are no extra (empty) messages
As an example with 50 trades, there will be int(50/50 + 0.99) = 1 message
"""
for i in range(0, max(int(len(statlist) / max_trades_per_msg + 0.99), 1)):
message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg],
headers=head,
tablefmt='simple')
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e))
@ -487,6 +501,10 @@ class Telegram(RPCHandler):
result = self._rpc._rpc_balance(self._config['stake_currency'],
self._config.get('fiat_display_currency', ''))
balance_dust_level = self._config['telegram'].get('balance_dust_level', 0.0)
if not balance_dust_level:
balance_dust_level = DUST_PER_COIN.get(self._config['stake_currency'], 1.0)
output = ''
if self._config['dry_run']:
output += (
@ -496,7 +514,7 @@ class Telegram(RPCHandler):
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n"
)
for curr in result['currencies']:
if curr['est_stake'] > 0.0001:
if curr['est_stake'] > balance_dust_level:
curr_output = (
f"*{curr['currency']}:*\n"
f"\t`Available: {curr['free']:.8f}`\n"
@ -505,7 +523,8 @@ class Telegram(RPCHandler):
f"\t`Est. {curr['stake']}: "
f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n")
else:
curr_output = f"*{curr['currency']}:* not showing <1$ amount \n"
curr_output = (f"*{curr['currency']}:* not showing <{balance_dust_level} "
f"{curr['stake']} amount \n")
# Handle overflowing messsage length
if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH:
@ -627,13 +646,13 @@ class Telegram(RPCHandler):
nrecent
)
trades_tab = tabulate(
[[arrow.get(trade['open_date']).humanize(),
trade['pair'],
[[arrow.get(trade['close_date']).humanize(),
trade['pair'] + " (#" + str(trade['trade_id']) + ")",
f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"]
for trade in trades['trades']],
headers=[
'Open Date',
'Pair',
'Close Date',
'Pair (ID)',
f'Profit ({stake_cur})',
],
tablefmt='simple')
@ -713,19 +732,35 @@ class Telegram(RPCHandler):
Handler for /locks.
Returns the currently active locks
"""
try:
locks = self._rpc._rpc_locks()
message = tabulate([[
lock['pair'],
lock['lock_end_time'],
lock['reason']] for lock in locks['locks']],
headers=['Pair', 'Until', 'Reason'],
tablefmt='simple')
message = "<pre>{}</pre>".format(message)
logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e))
locks = self._rpc._rpc_locks()
message = tabulate([[
lock['id'],
lock['pair'],
lock['lock_end_time'],
lock['reason']] for lock in locks['locks']],
headers=['ID', 'Pair', 'Until', 'Reason'],
tablefmt='simple')
message = f"<pre>{escape(message)}</pre>"
logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _delete_locks(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /delete_locks.
Returns the currently active locks
"""
arg = context.args[0] if context.args and len(context.args) > 0 else None
lockid = None
pair = None
if arg:
try:
lockid = int(arg)
except ValueError:
pair = arg
self._rpc._rpc_delete_lock(lockid=lockid, pair=pair)
self._locks(update, context)
@authorized_only
def _whitelist(self, update: Update, context: CallbackContext) -> None:
@ -844,6 +879,7 @@ class Telegram(RPCHandler):
"Avg. holding durationsfor buys and sells.`\n"
"*/count:* `Show number of active trades compared to allowed number of trades`\n"
"*/locks:* `Show currently locked pairs`\n"
"*/unlock <pair|id>:* `Unlock this Pair (or this lock id if it's numeric)`\n"
"*/balance:* `Show account balance per currency`\n"
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
"*/reload_config:* `Reload configuration file` \n"

View File

@ -28,6 +28,12 @@ class Webhook(RPCHandler):
self._url = self._config['webhook']['url']
self._format = self._config['webhook'].get('format', 'form')
if self._format != 'form' and self._format != 'json':
raise NotImplementedError('Unknown webhook format `{}`, possible values are '
'`form` (default) and `json`'.format(self._format))
def cleanup(self) -> None:
"""
Cleanup pending module resources.
@ -66,7 +72,14 @@ class Webhook(RPCHandler):
def _send_msg(self, payload: dict) -> None:
"""do the actual call to the webhook"""
if self._format == 'form':
kwargs = {'data': payload}
elif self._format == 'json':
kwargs = {'json': payload}
else:
raise NotImplementedError('Unknown format: {}'.format(self._format))
try:
post(self._url, data=payload)
post(self._url, **kwargs)
except RequestException as exc:
logger.warning("Could not call webhook url. Exception: %s", exc)

View File

@ -2,4 +2,4 @@
from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date,
timeframe_to_prev_date, timeframe_to_seconds)
from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_helper import merge_informative_pair
from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open

View File

@ -649,7 +649,7 @@ class IStrategy(ABC):
:return: True if bot should sell at current rate
"""
# Check if time matches and current rate is above threshold
trade_dur = int((current_time.timestamp() - trade.open_date.timestamp()) // 60)
trade_dur = int((current_time.timestamp() - trade.open_date_utc.timestamp()) // 60)
_, roi = self.min_roi_reached_entry(trade_dur)
if roi is None:
return False
@ -659,7 +659,7 @@ class IStrategy(ABC):
def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
"""
Populates indicators for given candle (OHLCV) data (for multiple pairs)
Does not run advice_buy or advise_sell!
Does not run advise_buy or advise_sell!
Used by optimize operations only, not during dry / live runs.
Using .copy() to get a fresh copy of the dataframe for every strategy run.
Has positive effects on memory usage for whatever reason - also when

View File

@ -56,3 +56,30 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
dataframe = dataframe.ffill()
return dataframe
def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float:
"""
Given the current profit, and a desired stop loss value relative to the open price,
return a stop loss value that is relative to the current price, and which can be
returned from `custom_stoploss`.
The requested stop can be positive for a stop above the open price, or negative for
a stop below the open price. The return value is always >= 0.
Returns 0 if the resulting stop price would be above the current price.
:param open_relative_stop: Desired stop loss percentage relative to open price
:param current_profit: The current profit percentage
:return: Positive stop loss value relative to current price
"""
# formula is undefined for current_profit -1, return maximum value
if current_profit == -1:
return 1
stoploss = 1-((1+open_relative_stop)/(1+current_profit))
# negative stoploss values indicate the requested stop price is higher than the current price
return max(stoploss, 0.0)

View File

@ -57,7 +57,8 @@
"enabled": false,
"listen_ip_address": "127.0.0.1",
"listen_port": 8080,
"verbosity": "info",
"verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "somethingrandom",
"CORS_origins": [],
"username": "",

View File

@ -39,6 +39,15 @@ class {{ hyperopt }}(IHyperOpt):
https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py.
"""
@staticmethod
def indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching buy strategy parameters.
"""
return [
{{ buy_space | indent(12) }}
]
@staticmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
@ -79,12 +88,12 @@ class {{ hyperopt }}(IHyperOpt):
return populate_buy_trend
@staticmethod
def indicator_space() -> List[Dimension]:
def sell_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching buy strategy parameters.
Define your Hyperopt space for searching sell strategy parameters.
"""
return [
{{ buy_space | indent(12) }}
{{ sell_space | indent(12) }}
]
@staticmethod
@ -126,11 +135,3 @@ class {{ hyperopt }}(IHyperOpt):
return populate_sell_trend
@staticmethod
def sell_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching sell strategy parameters.
"""
return [
{{ sell_space | indent(12) }}
]

View File

@ -45,6 +45,23 @@ class SampleHyperOpt(IHyperOpt):
https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py.
"""
@staticmethod
def indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching buy strategy parameters.
"""
return [
Integer(10, 25, name='mfi-value'),
Integer(15, 45, name='fastd-value'),
Integer(20, 50, name='adx-value'),
Integer(20, 40, name='rsi-value'),
Categorical([True, False], name='mfi-enabled'),
Categorical([True, False], name='fastd-enabled'),
Categorical([True, False], name='adx-enabled'),
Categorical([True, False], name='rsi-enabled'),
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
]
@staticmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
@ -92,20 +109,22 @@ class SampleHyperOpt(IHyperOpt):
return populate_buy_trend
@staticmethod
def indicator_space() -> List[Dimension]:
def sell_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching buy strategy parameters.
Define your Hyperopt space for searching sell strategy parameters.
"""
return [
Integer(10, 25, name='mfi-value'),
Integer(15, 45, name='fastd-value'),
Integer(20, 50, name='adx-value'),
Integer(20, 40, name='rsi-value'),
Categorical([True, False], name='mfi-enabled'),
Categorical([True, False], name='fastd-enabled'),
Categorical([True, False], name='adx-enabled'),
Categorical([True, False], name='rsi-enabled'),
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
Integer(75, 100, name='sell-mfi-value'),
Integer(50, 100, name='sell-fastd-value'),
Integer(50, 100, name='sell-adx-value'),
Integer(60, 100, name='sell-rsi-value'),
Categorical([True, False], name='sell-mfi-enabled'),
Categorical([True, False], name='sell-fastd-enabled'),
Categorical([True, False], name='sell-adx-enabled'),
Categorical([True, False], name='sell-rsi-enabled'),
Categorical(['sell-bb_upper',
'sell-macd_cross_signal',
'sell-sar_reversal'], name='sell-trigger')
]
@staticmethod
@ -153,56 +172,3 @@ class SampleHyperOpt(IHyperOpt):
return dataframe
return populate_sell_trend
@staticmethod
def sell_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching sell strategy parameters.
"""
return [
Integer(75, 100, name='sell-mfi-value'),
Integer(50, 100, name='sell-fastd-value'),
Integer(50, 100, name='sell-adx-value'),
Integer(60, 100, name='sell-rsi-value'),
Categorical([True, False], name='sell-mfi-enabled'),
Categorical([True, False], name='sell-fastd-enabled'),
Categorical([True, False], name='sell-adx-enabled'),
Categorical([True, False], name='sell-rsi-enabled'),
Categorical(['sell-bb_upper',
'sell-macd_cross_signal',
'sell-sar_reversal'], name='sell-trigger')
]
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators. Should be a copy of same method from strategy.
Must align to populate_indicators in this file.
Only used when --spaces does not include buy space.
"""
dataframe.loc[
(
(dataframe['close'] < dataframe['bb_lowerband']) &
(dataframe['mfi'] < 16) &
(dataframe['adx'] > 25) &
(dataframe['rsi'] < 21)
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators. Should be a copy of same method from strategy.
Must align to populate_indicators in this file.
Only used when --spaces does not include sell space.
"""
dataframe.loc[
(
(qtpylib.crossed_above(
dataframe['macdsignal'], dataframe['macd']
)) &
(dataframe['fastd'] > 54)
),
'sell'] = 1
return dataframe

View File

@ -60,6 +60,23 @@ class AdvancedSampleHyperOpt(IHyperOpt):
dataframe['sar'] = ta.SAR(dataframe)
return dataframe
@staticmethod
def indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching buy strategy parameters.
"""
return [
Integer(10, 25, name='mfi-value'),
Integer(15, 45, name='fastd-value'),
Integer(20, 50, name='adx-value'),
Integer(20, 40, name='rsi-value'),
Categorical([True, False], name='mfi-enabled'),
Categorical([True, False], name='fastd-enabled'),
Categorical([True, False], name='adx-enabled'),
Categorical([True, False], name='rsi-enabled'),
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
]
@staticmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
@ -106,20 +123,22 @@ class AdvancedSampleHyperOpt(IHyperOpt):
return populate_buy_trend
@staticmethod
def indicator_space() -> List[Dimension]:
def sell_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching strategy parameters
Define your Hyperopt space for searching sell strategy parameters.
"""
return [
Integer(10, 25, name='mfi-value'),
Integer(15, 45, name='fastd-value'),
Integer(20, 50, name='adx-value'),
Integer(20, 40, name='rsi-value'),
Categorical([True, False], name='mfi-enabled'),
Categorical([True, False], name='fastd-enabled'),
Categorical([True, False], name='adx-enabled'),
Categorical([True, False], name='rsi-enabled'),
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
Integer(75, 100, name='sell-mfi-value'),
Integer(50, 100, name='sell-fastd-value'),
Integer(50, 100, name='sell-adx-value'),
Integer(60, 100, name='sell-rsi-value'),
Categorical([True, False], name='sell-mfi-enabled'),
Categorical([True, False], name='sell-fastd-enabled'),
Categorical([True, False], name='sell-adx-enabled'),
Categorical([True, False], name='sell-rsi-enabled'),
Categorical(['sell-bb_upper',
'sell-macd_cross_signal',
'sell-sar_reversal'], name='sell-trigger')
]
@staticmethod
@ -168,25 +187,6 @@ class AdvancedSampleHyperOpt(IHyperOpt):
return populate_sell_trend
@staticmethod
def sell_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching sell strategy parameters
"""
return [
Integer(75, 100, name='sell-mfi-value'),
Integer(50, 100, name='sell-fastd-value'),
Integer(50, 100, name='sell-adx-value'),
Integer(60, 100, name='sell-rsi-value'),
Categorical([True, False], name='sell-mfi-enabled'),
Categorical([True, False], name='sell-fastd-enabled'),
Categorical([True, False], name='sell-adx-enabled'),
Categorical([True, False], name='sell-rsi-enabled'),
Categorical(['sell-bb_upper',
'sell-macd_cross_signal',
'sell-sar_reversal'], name='sell-trigger')
]
@staticmethod
def generate_roi_table(params: Dict) -> Dict[int, float]:
"""
@ -267,40 +267,3 @@ class AdvancedSampleHyperOpt(IHyperOpt):
Categorical([True, False], name='trailing_only_offset_is_reached'),
]
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators.
Can be a copy of the corresponding method from the strategy,
or will be loaded from the strategy.
Must align to populate_indicators used (either from this File, or from the strategy)
Only used when --spaces does not include buy
"""
dataframe.loc[
(
(dataframe['close'] < dataframe['bb_lowerband']) &
(dataframe['mfi'] < 16) &
(dataframe['adx'] > 25) &
(dataframe['rsi'] < 21)
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators.
Can be a copy of the corresponding method from the strategy,
or will be loaded from the strategy.
Must align to populate_indicators used (either from this File, or from the strategy)
Only used when --spaces does not include sell
"""
dataframe.loc[
(
(qtpylib.crossed_above(
dataframe['macdsignal'], dataframe['macd']
)) &
(dataframe['fastd'] > 54)
),
'sell'] = 1
return dataframe

View File

@ -10,7 +10,8 @@ import arrow
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
from freqtrade.exceptions import DependencyException
from freqtrade.exchange import Exchange
from freqtrade.persistence import Trade
from freqtrade.persistence import LocalTrade, Trade
from freqtrade.state import RunMode
logger = logging.getLogger(__name__)
@ -26,8 +27,9 @@ class Wallet(NamedTuple):
class Wallets:
def __init__(self, config: dict, exchange: Exchange) -> None:
def __init__(self, config: dict, exchange: Exchange, log: bool = True) -> None:
self._config = config
self._log = log
self._exchange = exchange
self._wallets: Dict[str, Wallet] = {}
self.start_cap = config['dry_run_wallet']
@ -64,9 +66,15 @@ class Wallets:
"""
# Recreate _wallets to reset closed trade balances
_wallets = {}
closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all()
open_trades = Trade.get_trades(Trade.is_open.is_(True)).all()
tot_profit = sum([trade.calc_profit() for trade in closed_trades])
open_trades = Trade.get_trades_proxy(is_open=True)
# If not backtesting...
# TODO: potentially remove the ._log workaround to determine backtest mode.
if self._log:
closed_trades = Trade.get_trades_proxy(is_open=False)
tot_profit = sum(
[trade.close_profit_abs for trade in closed_trades if trade.close_profit_abs])
else:
tot_profit = LocalTrade.total_profit
tot_in_trades = sum([trade.stake_amount for trade in open_trades])
current_stake = self.start_cap + tot_profit - tot_in_trades
@ -111,11 +119,12 @@ class Wallets:
:param require_update: Allow skipping an update if balances were recently refreshed
"""
if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().int_timestamp)):
if self._config['dry_run']:
self._update_dry()
else:
if (not self._config['dry_run'] or self._config.get('runmode') == RunMode.LIVE):
self._update_live()
logger.info('Wallets synced.')
else:
self._update_dry()
if self._log:
logger.info('Wallets synced.')
self._last_wallet_refresh = arrow.utcnow().int_timestamp
def get_all_balances(self) -> Dict[str, Any]:
@ -154,6 +163,7 @@ class Wallets:
Check if stake amount can be fulfilled with the available balance
for the stake currency
:return: float: Stake amount
:raise: DependencyException if balance is lower than stake-amount
"""
available_amount = self._get_available_stake_amount()

View File

@ -1,4 +1,5 @@
site_name: Freqtrade
repo_url: https://github.com/freqtrade/freqtrade
nav:
- Home: index.md
- Quickstart with Docker: docker_quickstart.md
@ -13,16 +14,16 @@ nav:
- Start the bot: bot-usage.md
- Control the bot:
- Telegram: telegram-usage.md
- Web Hook: webhook-config.md
- REST API & FreqUI: rest-api.md
- Web Hook: webhook-config.md
- Data Downloading: data-download.md
- Backtesting: backtesting.md
- Hyperopt: hyperopt.md
- Utility Sub-commands: utils.md
- Plotting: plotting.md
- Data Analysis:
- Jupyter Notebooks: data-analysis.md
- Strategy analysis: strategy_analysis_example.md
- Plotting: plotting.md
- Exchange-specific Notes: exchanges.md
- Advanced Topics:
- Advanced Post-installation Tasks: advanced-setup.md
@ -50,24 +51,25 @@ extra_javascript:
- https://polyfill.io/v3/polyfill.min.js?features=es6
- https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
markdown_extensions:
- admonition
- footnotes
- codehilite:
guess_lang: false
- toc:
permalink: true
- pymdownx.arithmatex:
generic: true
- pymdownx.details
- pymdownx.inlinehilite
- pymdownx.magiclink
- pymdownx.pathconverter
- pymdownx.smartsymbols
- pymdownx.snippets:
base_path: docs
check_paths: true
- pymdownx.tabbed
- pymdownx.superfences
- pymdownx.tasklist:
custom_checkbox: true
- mdx_truly_sane_lists
- attr_list
- admonition
- footnotes
- codehilite:
guess_lang: false
- toc:
permalink: true
- pymdownx.arithmatex:
generic: true
- pymdownx.details
- pymdownx.inlinehilite
- pymdownx.magiclink
- pymdownx.pathconverter
- pymdownx.smartsymbols
- pymdownx.snippets:
base_path: docs
check_paths: true
- pymdownx.tabbed
- pymdownx.superfences
- pymdownx.tasklist:
custom_checkbox: true
- mdx_truly_sane_lists

View File

@ -3,17 +3,17 @@
-r requirements-plot.txt
-r requirements-hyperopt.txt
coveralls==3.0.0
flake8==3.8.4
coveralls==3.0.1
flake8==3.9.0
flake8-type-annotations==0.1.0
flake8-tidy-imports==4.2.1
mypy==0.790
mypy==0.812
pytest==6.2.2
pytest-asyncio==0.14.0
pytest-cov==2.11.1
pytest-mock==3.5.1
pytest-random-order==1.0.4
isort==5.7.0
isort==5.8.0
# Convert jupyter notebooks to markdown documents
nbconvert==6.0.7

View File

@ -7,4 +7,5 @@ scikit-learn==0.24.1
scikit-optimize==0.8.1
filelock==3.0.12
joblib==1.0.1
psutil==5.8.0
progressbar2==3.53.1

View File

@ -1,16 +1,16 @@
numpy==1.20.1
pandas==1.2.2
pandas==1.2.3
ccxt==1.42.19
ccxt==1.43.89
# Pin cryptography for now due to rust build errors with piwheels
cryptography==3.4.6
aiohttp==3.7.3
SQLAlchemy==1.3.23
python-telegram-bot==13.3
arrow==0.17.0
aiohttp==3.7.4.post0
SQLAlchemy==1.4.2
python-telegram-bot==13.4.1
arrow==1.0.3
cachetools==4.2.1
requests==2.25.1
urllib3==1.26.3
urllib3==1.26.4
wrapt==1.12.1
jsonschema==3.2.0
TA-Lib==0.4.19
@ -39,4 +39,4 @@ aiofiles==0.6.0
colorama==0.4.4
# Building config files interactively
questionary==1.9.0
prompt-toolkit==3.0.16
prompt-toolkit==3.0.17

View File

@ -118,6 +118,14 @@ class FtRestClient():
"""
return self._get("locks")
def delete_lock(self, lock_id):
"""Delete (disable) lock from the database.
:param lock_id: ID for the lock to delete
:return: json object
"""
return self._delete("locks/{}".format(lock_id))
def daily(self, days=None):
"""Return the amount of open trades.
@ -174,6 +182,16 @@ class FtRestClient():
"""
return self._get("show_config")
def ping(self):
"""simple ping"""
configstatus = self.show_config()
if not configstatus:
return {"status": "not_running"}
elif configstatus['state'] == "running":
return {"status": "pong"}
else:
return {"status": "not_running"}
def logs(self, limit=None):
"""Show latest logs.

View File

@ -28,6 +28,7 @@ hyperopt = [
'filelock',
'joblib',
'progressbar2',
'psutil',
]
develop = [

View File

@ -706,7 +706,7 @@ def test_download_data_timerange(mocker, caplog, markets):
start_download_data(get_args(args))
assert dl_mock.call_count == 1
# 20days ago
days_ago = arrow.get(arrow.utcnow().shift(days=-20).date()).int_timestamp
days_ago = arrow.get(arrow.now().shift(days=-20).date()).int_timestamp
assert dl_mock.call_args_list[0][1]['timerange'].startts == days_ago
dl_mock.reset_mock()
@ -920,7 +920,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.load_previous_results',
'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results',
MagicMock(return_value=hyperopt_results)
)
@ -1145,14 +1145,14 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
captured = capsys.readouterr()
log_has("CSV file created: test_file.csv", caplog)
f = Path("test_file.csv")
assert 'Best,1,2,-1.25%,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text()
assert 'Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text()
assert f.is_file()
f.unlink()
def test_hyperopt_show(mocker, capsys, hyperopt_results):
mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.load_previous_results',
'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results',
MagicMock(return_value=hyperopt_results)
)

View File

@ -6,7 +6,7 @@ from copy import deepcopy
from datetime import datetime
from functools import reduce
from pathlib import Path
from unittest.mock import MagicMock, PropertyMock
from unittest.mock import MagicMock, Mock, PropertyMock
import arrow
import numpy as np
@ -19,7 +19,7 @@ from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.edge import Edge, PairInfo
from freqtrade.exchange import Exchange
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade, init_db
from freqtrade.persistence import LocalTrade, Trade, init_db
from freqtrade.resolvers import ExchangeResolver
from freqtrade.worker import Worker
from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4,
@ -64,6 +64,14 @@ def get_args(args):
return Arguments(args).get_parsed_arg()
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
def get_mock_coro(return_value):
async def mock_coro(*args, **kwargs):
return return_value
return Mock(wraps=mock_coro)
def patched_configuration_load_config_file(mocker, config) -> None:
mocker.patch(
'freqtrade.configuration.configuration.load_config_file',
@ -183,28 +191,34 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None:
freqtrade.exchange.refresh_latest_ohlcv = lambda p: None
def create_mock_trades(fee):
def create_mock_trades(fee, use_db: bool = True):
"""
Create some fake trades ...
"""
def add_trade(trade):
if use_db:
Trade.session.add(trade)
else:
LocalTrade.add_bt_trade(trade)
# Simulate dry_run entries
trade = mock_trade_1(fee)
Trade.session.add(trade)
add_trade(trade)
trade = mock_trade_2(fee)
Trade.session.add(trade)
add_trade(trade)
trade = mock_trade_3(fee)
Trade.session.add(trade)
add_trade(trade)
trade = mock_trade_4(fee)
Trade.session.add(trade)
add_trade(trade)
trade = mock_trade_5(fee)
Trade.session.add(trade)
add_trade(trade)
trade = mock_trade_6(fee)
Trade.session.add(trade)
add_trade(trade)
@pytest.fixture(autouse=True)
@ -255,6 +269,7 @@ def get_default_conf(testdatadir):
"20": 0.02,
"0": 0.04
},
"dry_run_wallet": 1000,
"stoploss": -0.10,
"unfilledtimeout": {
"buy": 10,
@ -1729,7 +1744,7 @@ def import_fails() -> None:
realimport = builtins.__import__
def mockedimport(name, *args, **kwargs):
if name in ["filelock", 'systemd.journal']:
if name in ["filelock", 'systemd.journal', 'uvloop']:
raise ImportError(f"No module named '{name}'")
return realimport(name, *args, **kwargs)
@ -1766,7 +1781,7 @@ def hyperopt_results():
'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, 'total_profit': -0.00125625, 'profit': -2.50999, 'duration': 3930.0}, # 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,
@ -1781,7 +1796,7 @@ def hyperopt_results():
'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, 'total_profit': 6.185e-05, 'profit': 0.12357, 'duration': 1200.0}, # noqa: E501
'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,
@ -1791,7 +1806,7 @@ def hyperopt_results():
'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, 'total_profit': -0.13639474, 'profit': -272.515306, 'duration': 1691.207729468599}, # 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,
@ -1801,14 +1816,14 @@ def hyperopt_results():
'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, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # 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, 'total_profit': -0.002480140000000001, 'profit': -4.955321, 'duration': 3402.8571428571427}, # 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,
@ -1818,7 +1833,7 @@ def hyperopt_results():
'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, 'total_profit': -0.0041773, 'profit': -8.346264999999997, 'duration': 636.9230769230769}, # 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,
@ -1830,7 +1845,7 @@ def hyperopt_results():
'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, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, # 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,
@ -1840,7 +1855,7 @@ def hyperopt_results():
'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, 'total_profit': 0.0, 'profit': 0.0, 'duration': 5340.0}, # 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,
@ -1850,7 +1865,7 @@ def hyperopt_results():
'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, 'total_profit': -0.044050070000000004, 'profit': -88.01256299999999, 'duration': 6505.676855895196}, # 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,
@ -1860,7 +1875,7 @@ def hyperopt_results():
'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, 'total_profit': 0.00021629, 'profit': 0.432154, 'duration': 2850.0}, # 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,
@ -1870,7 +1885,7 @@ def hyperopt_results():
'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, 'total_profit': -0.07436117, 'profit': -148.573727, 'duration': 4282.5641025641025}, # 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,
@ -1880,7 +1895,7 @@ def hyperopt_results():
'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, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # 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,

View File

@ -28,6 +28,8 @@ def mock_trade_1(fee):
amount_requested=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
is_open=True,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
open_rate=0.123,
exchange='bittrex',
open_order_id='dry_run_buy_12345',
@ -81,6 +83,7 @@ def mock_trade_2(fee):
open_rate=0.123,
close_rate=0.128,
close_profit=0.005,
close_profit_abs=0.000584127,
exchange='bittrex',
is_open=False,
open_order_id='dry_run_sell_12345',
@ -88,7 +91,7 @@ def mock_trade_2(fee):
timeframe=5,
sell_reason='sell_signal',
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc),
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
)
o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy')
trade.orders.append(o)
@ -140,6 +143,7 @@ def mock_trade_3(fee):
open_rate=0.05,
close_rate=0.06,
close_profit=0.01,
close_profit_abs=0.000155,
exchange='bittrex',
is_open=False,
strategy='DefaultStrategy',
@ -180,6 +184,8 @@ def mock_trade_4(fee):
amount_requested=124.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=14),
is_open=True,
open_rate=0.123,
exchange='bittrex',
open_order_id='prod_buy_12345',
@ -230,6 +236,8 @@ def mock_trade_5(fee):
amount_requested=124.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=12),
is_open=True,
open_rate=0.123,
exchange='bittrex',
strategy='SampleStrategy',
@ -279,8 +287,10 @@ def mock_trade_6(fee):
stake_amount=0.001,
amount=2.0,
amount_requested=2.0,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5),
fee_open=fee.return_value,
fee_close=fee.return_value,
is_open=True,
open_rate=0.15,
exchange='bittrex',
strategy='SampleStrategy',

View File

@ -274,15 +274,17 @@ def test_create_cum_profit1(testdatadir):
def test_calculate_max_drawdown(testdatadir):
filename = testdatadir / "backtest-result_test.json"
bt_data = load_backtest_data(filename)
drawdown, h, low = calculate_max_drawdown(bt_data)
drawdown, hdate, lowdate, hval, lval = calculate_max_drawdown(bt_data)
assert isinstance(drawdown, float)
assert pytest.approx(drawdown) == 0.21142322
assert isinstance(h, Timestamp)
assert isinstance(low, Timestamp)
assert h == Timestamp('2018-01-24 14:25:00', tz='UTC')
assert low == Timestamp('2018-01-30 04:45:00', tz='UTC')
assert isinstance(hdate, Timestamp)
assert isinstance(lowdate, Timestamp)
assert isinstance(hval, float)
assert isinstance(lval, float)
assert hdate == Timestamp('2018-01-24 14:25:00', tz='UTC')
assert lowdate == Timestamp('2018-01-30 04:45:00', tz='UTC')
with pytest.raises(ValueError, match='Trade dataframe empty.'):
drawdown, h, low = calculate_max_drawdown(DataFrame())
drawdown, hdate, lowdate, hval, lval = calculate_max_drawdown(DataFrame())
def test_calculate_csum(testdatadir):
@ -294,6 +296,10 @@ def test_calculate_csum(testdatadir):
assert isinstance(csum_max, float)
assert csum_min < 0.01
assert csum_max > 0.02
csum_min1, csum_max1 = calculate_csum(bt_data, 5)
assert csum_min1 == csum_min + 5
assert csum_max1 == csum_max + 5
with pytest.raises(ValueError, match='Trade dataframe empty.'):
csum_min, csum_max = calculate_csum(DataFrame())
@ -310,13 +316,16 @@ def test_calculate_max_drawdown2():
# sort by profit and reset index
df = df.sort_values('profit').reset_index(drop=True)
df1 = df.copy()
drawdown, h, low = calculate_max_drawdown(df, date_col='open_date', value_col='profit')
drawdown, hdate, ldate, hval, lval = calculate_max_drawdown(
df, date_col='open_date', value_col='profit')
# Ensure df has not been altered.
assert df.equals(df1)
assert isinstance(drawdown, float)
# High must be before low
assert h < low
assert hdate < ldate
# High value must be higher than low value
assert hval > lval
assert drawdown == 0.091755
df = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_date'])

View File

@ -44,6 +44,7 @@ EXCHANGES = {
def exchange_conf():
config = get_default_conf((Path(__file__).parent / "testdata").resolve())
config['exchange']['pair_whitelist'] = []
config['dry_run'] = False
return config

View File

@ -1,6 +1,7 @@
import copy
import logging
from datetime import datetime, timedelta, timezone
from math import isclose
from random import randint
from unittest.mock import MagicMock, Mock, PropertyMock, patch
@ -18,21 +19,13 @@ from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes,
timeframe_to_next_date, timeframe_to_prev_date,
timeframe_to_seconds)
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import get_patched_exchange, log_has, log_has_re
from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has_re
# Make sure to always keep one exchange here which is NOT subclassed!!
EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx']
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
def get_mock_coro(return_value):
async def mock_coro(*args, **kwargs):
return return_value
return Mock(wraps=mock_coro)
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs):
@ -378,7 +371,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss)
assert result == 2 / 0.9
assert isclose(result, 2 * 1.1)
# min amount is set
markets["ETH/BTC"]["limits"] = {
@ -390,7 +383,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
assert result == 2 * 2 / 0.9
assert isclose(result, 2 * 2 * 1.1)
# min amount and cost are set (cost is minimal)
markets["ETH/BTC"]["limits"] = {
@ -402,7 +395,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
assert result == max(2, 2 * 2) / 0.9
assert isclose(result, max(2, 2 * 2) * 1.1)
# min amount and cost are set (amount is minial)
markets["ETH/BTC"]["limits"] = {
@ -414,7 +407,14 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
assert result == max(8, 2 * 2) / 0.9
assert isclose(result, max(8, 2 * 2) * 1.1)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4)
assert isclose(result, max(8, 2 * 2) * 1.45)
# Really big stoploss
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1)
assert isclose(result, max(8, 2 * 2) * 1.5)
def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
@ -432,7 +432,7 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss)
assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) / 0.9, 8)
assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * 1.1, 8)
def test_set_sandbox(default_conf, mocker):
@ -498,7 +498,7 @@ def test__load_markets(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
Exchange(default_conf)
assert log_has('Unable to initialize markets. Reason: SomeError', caplog)
assert log_has('Unable to initialize markets.', caplog)
expected_return = {'ETH/BTC': 'available'}
api_mock = MagicMock()
@ -2276,12 +2276,20 @@ def test_get_fee(default_conf, mocker, exchange_name):
'cost': 0.05
})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange._config.pop('fee', None)
assert exchange.get_fee('ETH/BTC') == 0.025
assert api_mock.calculate_fee.call_count == 1
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
'get_fee', 'calculate_fee', symbol="ETH/BTC")
api_mock.calculate_fee.reset_mock()
exchange._config['fee'] = 0.001
assert exchange.get_fee('ETH/BTC') == 0.001
assert api_mock.calculate_fee.call_count == 0
def test_stoploss_order_unsupported_exchange(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id='bittrex')

View File

@ -1,6 +1,5 @@
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, C0330, unused-argument
import logging
from unittest.mock import MagicMock
import pytest
@ -489,7 +488,8 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset
default_conf["ask_strategy"] = {"use_sell_signal": data.use_sell_signal}
mocker.patch("freqtrade.exchange.Exchange.get_fee", MagicMock(return_value=0.0))
mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
patch_exchange(mocker)
frame = _build_backtest_dataframe(data.data)
backtesting = Backtesting(default_conf)
@ -503,7 +503,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
min_date, max_date = get_timerange({pair: frame})
results = backtesting.backtest(
processed=data_processed,
stake_amount=default_conf['stake_amount'],
start_date=min_date,
end_date=max_date,
max_open_trades=10,
@ -514,6 +513,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
for c, trade in enumerate(data.trades):
res = results.iloc[c]
assert res.sell_reason == trade.sell_reason
assert res.sell_reason == trade.sell_reason.value
assert res.open_date == _get_frame_time_from_offset(trade.open_tick)
assert res.close_date == _get_frame_time_from_offset(trade.close_tick)

View File

@ -9,7 +9,6 @@ import pandas as pd
import pytest
from arrow import Arrow
from freqtrade import constants
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting
from freqtrade.configuration import TimeRange
from freqtrade.data import history
@ -19,6 +18,7 @@ from freqtrade.data.dataprovider import DataProvider
from freqtrade.data.history import get_timerange
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.optimize.backtesting import Backtesting
from freqtrade.persistence import LocalTrade
from freqtrade.resolvers import StrategyResolver
from freqtrade.state import RunMode
from freqtrade.strategy.interface import SellType
@ -90,7 +90,6 @@ def simple_backtest(config, contour, mocker, testdatadir) -> None:
assert isinstance(processed, dict)
results = backtesting.backtest(
processed=processed,
stake_amount=config['stake_amount'],
start_date=min_date,
end_date=max_date,
max_open_trades=1,
@ -111,7 +110,6 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
min_date, max_date = get_timerange(processed)
return {
'processed': processed,
'stake_amount': conf['stake_amount'],
'start_date': min_date,
'end_date': max_date,
'max_open_trades': 10,
@ -233,8 +231,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
assert log_has('Parameter --fee detected, setting fee to: {} ...'.format(config['fee']), caplog)
def test_setup_optimize_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None:
default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog) -> None:
patched_configuration_load_config_file(mocker, default_conf)
@ -242,9 +239,21 @@ def test_setup_optimize_configuration_unlimited_stake_amount(mocker, default_con
'backtesting',
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'--stake-amount', '1',
'--starting-balance', '2'
]
with pytest.raises(DependencyException, match=r'.`stake_amount`.*'):
conf = setup_optimize_configuration(get_args(args), RunMode.BACKTEST)
assert isinstance(conf, dict)
args = [
'backtesting',
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'--stake-amount', '1',
'--starting-balance', '0.5'
]
with pytest.raises(OperationalException, match=r"Starting balance .* smaller .*"):
setup_optimize_configuration(get_args(args), RunMode.BACKTEST)
@ -448,9 +457,48 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti
Backtesting(default_conf)
def test_backtest__enter_trade(default_conf, fee, mocker, testdatadir) -> None:
default_conf['ask_strategy']['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['stake_amount'] = 'unlimited'
backtesting = Backtesting(default_conf)
pair = 'UNITTEST/BTC'
row = [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0),
1, # Sell
0.001, # Open
0.0011, # Close
0, # Sell
0.00099, # Low
0.0012, # High
]
trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0)
assert isinstance(trade, LocalTrade)
assert trade.stake_amount == 495
trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=2)
assert trade is None
# Stake-amount too high!
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0)
trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0)
assert trade is None
# Stake-amount too high!
mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount",
side_effect=DependencyException)
trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0)
assert trade is None
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
default_conf['ask_strategy']['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)
backtesting = Backtesting(default_conf)
pair = 'UNITTEST/BTC'
@ -461,7 +509,6 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
min_date, max_date = get_timerange(processed)
results = backtesting.backtest(
processed=processed,
stake_amount=default_conf['stake_amount'],
start_date=min_date,
end_date=max_date,
max_open_trades=10,
@ -486,7 +533,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
'trade_duration': [235, 40],
'profit_ratio': [0.0, 0.0],
'profit_abs': [0.0, 0.0],
'sell_reason': [SellType.ROI, SellType.ROI],
'sell_reason': [SellType.ROI.value, SellType.ROI.value],
'initial_stop_loss_abs': [0.0940005, 0.09272236],
'initial_stop_loss_ratio': [-0.1, -0.1],
'stop_loss_abs': [0.0940005, 0.09272236],
@ -512,6 +559,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None:
default_conf['ask_strategy']['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)
backtesting = Backtesting(default_conf)
@ -523,7 +571,6 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
min_date, max_date = get_timerange(processed)
results = backtesting.backtest(
processed=processed,
stake_amount=default_conf['stake_amount'],
start_date=min_date,
end_date=max_date,
max_open_trades=1,
@ -558,6 +605,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
default_conf['enable_protections'] = True
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
tests = [
['sine', 9],
['raise', 10],
@ -589,6 +637,7 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir,
default_conf['protections'] = protections
default_conf['enable_protections'] = True
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
# While buy-signals are unrealistic, running backtesting
# over and over again should not cause different results
@ -626,6 +675,7 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir):
def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf,
pair='UNITTEST/BTC', datadir=testdatadir)
@ -658,6 +708,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
dataframe['sell'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0)
return dataframe
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker)
@ -678,7 +729,6 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
min_date, max_date = get_timerange(processed)
backtest_conf = {
'processed': processed,
'stake_amount': default_conf['stake_amount'],
'start_date': min_date,
'end_date': max_date,
'max_open_trades': 3,
@ -694,7 +744,6 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
backtest_conf = {
'processed': processed,
'stake_amount': default_conf['stake_amount'],
'start_date': min_date,
'end_date': max_date,
'max_open_trades': 1,
@ -822,6 +871,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'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]
@ -838,6 +888,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'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]

View File

@ -12,11 +12,11 @@ import pytest
from arrow import Arrow
from filelock import Timeout
from freqtrade import constants
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt
from freqtrade.data.history import load_data
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exceptions import OperationalException
from freqtrade.optimize.hyperopt import Hyperopt
from freqtrade.optimize.hyperopt_tools import HyperoptTools
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
from freqtrade.state import RunMode
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
@ -130,8 +130,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo
assert log_has('Parameter --print-all detected ...', caplog)
def test_setup_hyperopt_configuration_unlimited_stake_amount(mocker, default_conf) -> None:
default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None:
patched_configuration_load_config_file(mocker, default_conf)
@ -139,9 +138,20 @@ def test_setup_hyperopt_configuration_unlimited_stake_amount(mocker, default_con
'hyperopt',
'--config', 'config.json',
'--hyperopt', 'DefaultHyperOpt',
'--stake-amount', '1',
'--starting-balance', '2'
]
conf = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
assert isinstance(conf, dict)
with pytest.raises(DependencyException, match=r'.`stake_amount`.*'):
args = [
'hyperopt',
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'--stake-amount', '1',
'--starting-balance', '0.5'
]
with pytest.raises(OperationalException, match=r"Starting balance .* smaller .*"):
setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
@ -327,9 +337,9 @@ def test_save_results_saves_epochs(mocker, hyperopt, testdatadir, caplog) -> Non
def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> None:
epochs = create_results(mocker, hyperopt, testdatadir)
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs)
mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs)
results_file = testdatadir / 'optimize' / 'ut_results.pickle'
hyperopt_epochs = hyperopt._read_results(results_file)
hyperopt_epochs = HyperoptTools._read_results(results_file)
assert log_has(f"Reading epochs from '{results_file}'", caplog)
assert hyperopt_epochs == epochs
mock_load.assert_called_once()
@ -337,7 +347,7 @@ def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> N
def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None:
epochs = create_results(mocker, hyperopt, testdatadir)
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs)
mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs)
mocker.patch.object(Path, 'is_file', MagicMock(return_value=True))
statmock = MagicMock()
statmock.st_size = 5
@ -345,16 +355,16 @@ def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None:
results_file = testdatadir / 'optimize' / 'ut_results.pickle'
hyperopt_epochs = hyperopt.load_previous_results(results_file)
hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
assert hyperopt_epochs == epochs
mock_load.assert_called_once()
del epochs[0]['is_best']
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs)
mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs)
with pytest.raises(OperationalException):
hyperopt.load_previous_results(results_file)
HyperoptTools.load_previous_results(results_file)
def test_roi_table_generation(hyperopt) -> None:
@ -444,7 +454,7 @@ def test_format_results(hyperopt):
'is_initial_point': True,
}
result = hyperopt._format_explanation_string(results, 1)
result = HyperoptTools._format_explanation_string(results, 1)
assert result.find(' 66.67%')
assert result.find('Total profit 1.00000000 BTC')
assert result.find('2.0000Σ %')
@ -458,7 +468,7 @@ def test_format_results(hyperopt):
df = pd.DataFrame.from_records(trades, columns=labels)
results_metrics = hyperopt._calculate_results_metrics(df)
results['total_profit'] = results_metrics['total_profit']
result = hyperopt._format_explanation_string(results, 1)
result = HyperoptTools._format_explanation_string(results, 1)
assert result.find('Total profit 1.00000000 EUR')
@ -1067,7 +1077,7 @@ def test_print_epoch_details(capsys):
'is_best': True
}
Hyperopt.print_epoch_details(test_result, 5, False, no_header=True)
HyperoptTools.print_epoch_details(test_result, 5, False, no_header=True)
captured = capsys.readouterr()
assert '# Trailing stop:' in captured.out
# re.match(r"Pairs for .*", captured.out)

View File

@ -48,7 +48,7 @@ def test_text_table_bt_results():
)
pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC',
max_open_trades=2, results=results)
starting_balance=4, results=results)
assert text_table_bt_results(pair_results, stake_currency='BTC') == result_str
@ -73,11 +73,13 @@ def test_generate_backtest_stats(default_conf, testdatadir):
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
"trade_duration": [123, 34, 31, 14],
"is_open": [False, False, False, True],
"stake_amount": [0.01, 0.01, 0.01, 0.01],
"sell_reason": [SellType.ROI, SellType.STOP_LOSS,
SellType.ROI, SellType.FORCE_SELL]
}),
'config': default_conf,
'locks': [],
'final_balance': 1000.02,
'backtest_start_time': Arrow.utcnow().int_timestamp,
'backtest_end_time': Arrow.utcnow().int_timestamp,
}
@ -100,6 +102,7 @@ def test_generate_backtest_stats(default_conf, testdatadir):
# Above sample had no loosing trade
assert strat_stats['max_drawdown'] == 0.0
# Retry with losing trade
results = {'DefStrat': {
'results': pd.DataFrame(
{"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"],
@ -116,18 +119,31 @@ def test_generate_backtest_stats(default_conf, testdatadir):
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
"close_rate": [0.002546, 0.003014, 0.0032903, 0.003217],
"trade_duration": [123, 34, 31, 14],
"open_at_end": [False, False, False, True],
"sell_reason": [SellType.ROI, SellType.STOP_LOSS,
SellType.ROI, SellType.FORCE_SELL]
"is_open": [False, False, False, True],
"stake_amount": [0.01, 0.01, 0.01, 0.01],
"sell_reason": [SellType.ROI, SellType.ROI,
SellType.STOP_LOSS, SellType.FORCE_SELL]
}),
'config': default_conf}
'config': default_conf,
'locks': [],
'final_balance': 1000.02,
'backtest_start_time': Arrow.utcnow().int_timestamp,
'backtest_end_time': Arrow.utcnow().int_timestamp,
}
}
assert strat_stats['max_drawdown'] == 0.0
assert strat_stats['drawdown_start'] == datetime(1970, 1, 1, tzinfo=timezone.utc)
assert strat_stats['drawdown_end'] == datetime(1970, 1, 1, tzinfo=timezone.utc)
assert strat_stats['drawdown_end_ts'] == 0
assert strat_stats['drawdown_start_ts'] == 0
stats = generate_backtest_stats(btdata, results, min_date, max_date)
assert isinstance(stats, dict)
assert 'strategy' in stats
assert 'DefStrat' in stats['strategy']
assert 'strategy_comparison' in stats
strat_stats = stats['strategy']['DefStrat']
assert strat_stats['max_drawdown'] == 0.013803
assert strat_stats['drawdown_start'] == datetime(2017, 11, 14, 22, 10, tzinfo=timezone.utc)
assert strat_stats['drawdown_end'] == datetime(2017, 11, 14, 22, 43, tzinfo=timezone.utc)
assert strat_stats['drawdown_end_ts'] == 1510699380000
assert strat_stats['drawdown_start_ts'] == 1510697400000
assert strat_stats['pairlist'] == ['UNITTEST/BTC']
# Test storing stats
@ -189,7 +205,7 @@ def test_generate_pair_metrics():
)
pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC',
max_open_trades=2, results=results)
starting_balance=2, results=results)
assert isinstance(pair_results, list)
assert len(pair_results) == 2
assert pair_results[-1]['key'] == 'TOTAL'
@ -265,7 +281,7 @@ def test_generate_sell_reason_stats():
'wins': [2, 0, 0],
'draws': [0, 0, 0],
'losses': [0, 0, 1],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
'sell_reason': [SellType.ROI.value, SellType.ROI.value, SellType.STOP_LOSS.value]
}
)
@ -291,6 +307,7 @@ def test_generate_sell_reason_stats():
def test_text_table_strategy(default_conf):
default_conf['max_open_trades'] = 2
default_conf['dry_run_wallet'] = 3
results = {}
results['TestStrategy1'] = {'results': pd.DataFrame(
{
@ -323,9 +340,9 @@ def test_text_table_strategy(default_conf):
'|---------------+--------+----------------+----------------+------------------+'
'----------------+----------------+--------+---------+----------|\n'
'| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 |'
' 30.00 | 0:17:00 | 3 | 0 | 0 |\n'
' 36.67 | 0:17:00 | 3 | 0 | 0 |\n'
'| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |'
' 45.00 | 0:20:00 | 3 | 0 | 0 |'
' 43.33 | 0:20:00 | 3 | 0 | 0 |'
)
strategy_results = generate_strategy_metrics(all_results=results)

View File

@ -73,9 +73,13 @@ def test_PairLocks(use_db):
assert PairLocks.is_pair_locked('XRP/USDT', lock_time + timedelta(minutes=-50))
if use_db:
assert len(PairLock.query.all()) > 0
locks = PairLocks.get_all_locks()
locks_db = PairLock.query.all()
assert len(locks) == len(locks_db)
assert len(locks_db) > 0
else:
# Nothing was pushed to the database
assert len(PairLocks.get_all_locks()) > 0
assert len(PairLock.query.all()) == 0
# Reset use-db variable
PairLocks.reset_locks()

View File

@ -1,7 +1,7 @@
# pragma pylint: disable=missing-docstring, C0103
# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments
from datetime import datetime
from datetime import datetime, timedelta, timezone
from unittest.mock import ANY, MagicMock, PropertyMock
import pytest
@ -10,6 +10,7 @@ from numpy import isnan
from freqtrade.edge import PairInfo
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
from freqtrade.persistence import Trade
from freqtrade.persistence.pairlock_middleware import PairLocks
from freqtrade.rpc import RPC, RPCException
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.state import State
@ -412,10 +413,10 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
assert prec_satoshi(stats['profit_closed_percent'], 6.2)
assert prec_satoshi(stats['profit_closed_percent_mean'], 6.2)
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
assert prec_satoshi(stats['profit_all_coin'], 5.802e-05)
assert prec_satoshi(stats['profit_all_percent'], 2.89)
assert prec_satoshi(stats['profit_all_percent_mean'], 2.89)
assert prec_satoshi(stats['profit_all_fiat'], 0.8703)
assert stats['trade_count'] == 2
assert stats['first_trade_date'] == 'just now'
@ -481,10 +482,10 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert prec_satoshi(stats['profit_closed_coin'], 0)
assert prec_satoshi(stats['profit_closed_percent'], 0)
assert prec_satoshi(stats['profit_closed_percent_mean'], 0)
assert prec_satoshi(stats['profit_closed_fiat'], 0)
assert prec_satoshi(stats['profit_all_coin'], 0)
assert prec_satoshi(stats['profit_all_percent'], 0)
assert prec_satoshi(stats['profit_all_percent_mean'], 0)
assert prec_satoshi(stats['profit_all_fiat'], 0)
assert stats['trade_count'] == 1
assert stats['first_trade_date'] == 'just now'
@ -911,6 +912,24 @@ def test_rpcforcebuy_disabled(mocker, default_conf) -> None:
rpc._rpc_forcebuy(pair, None)
@pytest.mark.usefixtures("init_persistence")
def test_rpc_delete_lock(mocker, default_conf):
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc = RPC(freqtradebot)
pair = 'ETH/BTC'
PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=4))
PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=5))
PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=10))
locks = rpc._rpc_locks()
assert locks['lock_count'] == 3
locks1 = rpc._rpc_delete_lock(lockid=locks['locks'][0]['id'])
assert locks1['lock_count'] == 2
locks2 = rpc._rpc_delete_lock(pair=pair)
assert locks2['lock_count'] == 0
def test_rpc_whitelist(mocker, default_conf) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())

View File

@ -23,8 +23,8 @@ from freqtrade.rpc.api_server import ApiServer
from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
from freqtrade.state import RunMode, State
from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re,
patch_get_signal)
from tests.conftest import (create_mock_trades, get_mock_coro, get_patched_freqtradebot, log_has,
log_has_re, patch_get_signal)
BASE_URI = "/api/v1"
@ -230,7 +230,7 @@ def test_api__init__(default_conf, mocker):
assert apiserver._config == default_conf
def test_api_UvicornServer(default_conf, mocker):
def test_api_UvicornServer(mocker):
thread_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.threading.Thread')
s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1'))
assert thread_mock.call_count == 0
@ -248,6 +248,38 @@ def test_api_UvicornServer(default_conf, mocker):
assert s.should_exit is True
def test_api_UvicornServer_run(mocker):
serve_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.UvicornServer.serve',
get_mock_coro(None))
s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1'))
assert serve_mock.call_count == 0
s.install_signal_handlers()
# Original implementation starts a thread - make sure that's not the case
assert serve_mock.call_count == 0
# Fake started to avoid sleeping forever
s.started = True
s.run()
assert serve_mock.call_count == 1
def test_api_UvicornServer_run_no_uvloop(mocker, import_fails):
serve_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.UvicornServer.serve',
get_mock_coro(None))
s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1'))
assert serve_mock.call_count == 0
s.install_signal_handlers()
# Original implementation starts a thread - make sure that's not the case
assert serve_mock.call_count == 0
# Fake started to avoid sleeping forever
s.started = True
s.run()
assert serve_mock.call_count == 1
def test_api_run(default_conf, mocker, caplog):
default_conf.update({"api_server": {"enabled": True,
"listen_ip_address": "127.0.0.1",
@ -418,6 +450,16 @@ def test_api_locks(botclient):
assert 'randreason' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason'])
assert 'deadbeef' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason'])
# Test deletions
rc = client_delete(client, f"{BASE_URI}/locks/1")
assert_response(rc)
assert rc.json()['lock_count'] == 1
rc = client_post(client, f"{BASE_URI}/locks/delete",
data='{"pair": "XRP/BTC"}')
assert_response(rc)
assert rc.json()['lock_count'] == 0
def test_api_show_config(botclient, mocker):
ftbot, client = botclient
@ -614,14 +656,12 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
'latest_trade_timestamp': ANY,
'profit_all_coin': 6.217e-05,
'profit_all_fiat': 0.76748865,
'profit_all_percent': 6.2,
'profit_all_percent_mean': 6.2,
'profit_all_ratio_mean': 0.06201058,
'profit_all_percent_sum': 6.2,
'profit_all_ratio_sum': 0.06201058,
'profit_closed_coin': 6.217e-05,
'profit_closed_fiat': 0.76748865,
'profit_closed_percent': 6.2,
'profit_closed_ratio_mean': 0.06201058,
'profit_closed_percent_mean': 6.2,
'profit_closed_ratio_sum': 0.06201058,
@ -770,14 +810,12 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
'stoploss_entry_dist_ratio': -0.10448878,
'trade_id': 1,
'close_rate_requested': None,
'current_rate': 1.099e-05,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'open_date': ANY,
'is_open': True,
'max_rate': 1.099e-05,
'min_rate': 1.098e-05,

View File

@ -92,7 +92,8 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], "
"['delete'], ['performance'], ['stats'], ['daily'], ['count'], ['locks'], "
"['reload_config', 'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], "
"['unlock', 'delete_locks'], ['reload_config', 'reload_conf'], "
"['show_config', 'show_conf'], ['stopbuy'], "
"['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']"
"]")
@ -520,7 +521,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick
assert 'Balance:' in result
assert 'Est. BTC:' in result
assert 'BTC: 12.00000000' in result
assert '*XRP:* not showing <1$ amount' in result
assert '*XRP:* not showing <0.0001 BTC amount' in result
def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
@ -981,6 +982,16 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None
assert 'deadbeef' in msg_mock.call_args_list[0][0][0]
assert 'randreason' in msg_mock.call_args_list[0][0][0]
context = MagicMock()
context.args = ['XRP/BTC']
msg_mock.reset_mock()
telegram._delete_locks(update=update, context=context)
assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0]
assert 'randreason' in msg_mock.call_args_list[0][0][0]
assert 'XRP/BTC' not in msg_mock.call_args_list[0][0][0]
assert 'deadbeef' not in msg_mock.call_args_list[0][0][0]
def test_whitelist_static(default_conf, update, mocker) -> None:
@ -1117,8 +1128,10 @@ def test_telegram_trades(mocker, update, default_conf, fee):
msg_mock.call_count == 1
assert "2 recent trades</b>:" in msg_mock.call_args_list[0][0][0]
assert "Profit (" in msg_mock.call_args_list[0][0][0]
assert "Open Date" in msg_mock.call_args_list[0][0][0]
assert "Close Date" in msg_mock.call_args_list[0][0][0]
assert "<pre>" in msg_mock.call_args_list[0][0][0]
assert bool(re.search(r"just now[ ]*XRP\/BTC \(#3\) 1.00% \(",
msg_mock.call_args_list[0][0][0]))
def test_telegram_delete_trade(mocker, update, default_conf, fee):
@ -1185,6 +1198,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None:
msg = {
'type': RPCMessageType.BUY_NOTIFICATION,
'trade_id': 1,
'exchange': 'Bittrex',
'pair': 'ETH/BTC',
'limit': 1.099e-05,
@ -1201,7 +1215,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None:
telegram.send_msg(msg)
assert msg_mock.call_args[0][0] \
== '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \
== '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n' \
'*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00001099`\n' \
'*Current Rate:* `0.00001099`\n' \
@ -1229,12 +1243,14 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None:
telegram.send_msg({
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Bittrex',
'pair': 'ETH/BTC',
'reason': CANCEL_REASON['TIMEOUT']
})
assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Bittrex:* '
'Cancelling open buy Order for ETH/BTC. Reason: cancelled due to timeout.')
'Cancelling open buy Order for ETH/BTC (#1). '
'Reason: cancelled due to timeout.')
def test_send_msg_sell_notification(default_conf, mocker) -> None:
@ -1245,6 +1261,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812
telegram.send_msg({
'type': RPCMessageType.SELL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Binance',
'pair': 'KEY/ETH',
'gain': 'loss',
@ -1262,7 +1279,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'close_date': arrow.utcnow(),
})
assert msg_mock.call_args[0][0] \
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n'
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
'*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n'
@ -1274,6 +1291,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
msg_mock.reset_mock()
telegram.send_msg({
'type': RPCMessageType.SELL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Binance',
'pair': 'KEY/ETH',
'gain': 'loss',
@ -1290,7 +1308,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'close_date': arrow.utcnow(),
})
assert msg_mock.call_args[0][0] \
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n'
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
'*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n'
@ -1310,23 +1328,26 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812
telegram.send_msg({
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Binance',
'pair': 'KEY/ETH',
'reason': 'Cancelled on exchange'
})
assert msg_mock.call_args[0][0] \
== ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. '
'Reason: Cancelled on exchange')
== ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).'
' Reason: Cancelled on exchange')
msg_mock.reset_mock()
telegram.send_msg({
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Binance',
'pair': 'KEY/ETH',
'reason': 'timeout'
})
assert msg_mock.call_args[0][0] \
== ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: timeout')
== ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).'
' Reason: timeout')
# Reset singleton function to avoid random breaks
telegram._rpc._fiat_converter.convert_amount = old_convamount
@ -1373,6 +1394,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
telegram.send_msg({
'type': RPCMessageType.BUY_NOTIFICATION,
'trade_id': 1,
'exchange': 'Bittrex',
'pair': 'ETH/BTC',
'limit': 1.099e-05,
@ -1385,7 +1407,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
'amount': 1333.3333333333335,
'open_date': arrow.utcnow().shift(hours=-1)
})
assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n'
assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n'
'*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00001099`\n'
'*Current Rate:* `0.00001099`\n'
@ -1398,6 +1420,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
telegram.send_msg({
'type': RPCMessageType.SELL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Binance',
'pair': 'KEY/ETH',
'gain': 'loss',
@ -1414,7 +1437,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3),
'close_date': arrow.utcnow(),
})
assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n'
assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
'*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n'

View File

@ -225,3 +225,15 @@ def test__send_msg(default_conf, mocker, caplog):
mocker.patch("freqtrade.rpc.webhook.post", post)
webhook._send_msg(msg)
assert log_has('Could not call webhook url. Exception: ', caplog)
def test__send_msg_with_json_format(default_conf, mocker, caplog):
default_conf["webhook"] = get_webhook_dict()
default_conf["webhook"]["format"] = "json"
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
msg = {'text': 'Hello'}
post = MagicMock()
mocker.patch("freqtrade.rpc.webhook.post", post)
webhook._send_msg(msg)
assert post.call_args[1] == {'json': msg}

View File

@ -1,8 +1,10 @@
from math import isclose
import numpy as np
import pandas as pd
import pytest
from freqtrade.strategy import merge_informative_pair, timeframe_to_minutes
from freqtrade.strategy import merge_informative_pair, stoploss_from_open, timeframe_to_minutes
def generate_test_data(timeframe: str, size: int):
@ -95,3 +97,38 @@ def test_merge_informative_pair_lower():
with pytest.raises(ValueError, match=r"Tried to merge a faster timeframe .*"):
merge_informative_pair(data, informative, '1h', '15m', ffill=True)
def test_stoploss_from_open():
open_price_ranges = [
[0.01, 1.00, 30],
[1, 100, 30],
[100, 10000, 30],
]
current_profit_range = [-0.99, 2, 30]
desired_stop_range = [-0.50, 0.50, 30]
for open_range in open_price_ranges:
for open_price in np.linspace(*open_range):
for desired_stop in np.linspace(*desired_stop_range):
# -1 is not a valid current_profit, should return 1
assert stoploss_from_open(desired_stop, -1) == 1
for current_profit in np.linspace(*current_profit_range):
current_price = open_price * (1 + current_profit)
expected_stop_price = open_price * (1 + desired_stop)
stoploss = stoploss_from_open(desired_stop, current_profit)
assert stoploss >= 0
assert stoploss <= 1
stop_price = current_price * (1 - stoploss)
# there is no correct answer if the expected stop price is above
# the current price
if expected_stop_price > current_price:
assert stoploss == 0
else:
assert isclose(stop_price, expected_stop_price, rel_tol=0.00001)

View File

@ -430,7 +430,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
'--enable-position-stacking',
'--disable-max-market-positions',
'--timerange', ':100',
'--export', '/bar/foo'
'--export', '/bar/foo',
'--stake-amount', 'unlimited'
]
args = Arguments(arglist).get_parsed_arg()
@ -463,6 +464,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
assert 'export' in config
assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog)
assert 'stake_amount' in config
assert config['stake_amount'] == 'unlimited'
def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None:
@ -787,6 +790,38 @@ def test_validate_max_open_trades(default_conf):
validate_config_consistency(default_conf)
def test_validate_price_side(default_conf):
default_conf['order_types'] = {
"buy": "limit",
"sell": "limit",
"stoploss": "limit",
"stoploss_on_exchange": False,
}
# Default should pass
validate_config_consistency(default_conf)
conf = deepcopy(default_conf)
conf['order_types']['buy'] = 'market'
with pytest.raises(OperationalException,
match='Market buy orders require bid_strategy.price_side = "ask".'):
validate_config_consistency(conf)
conf = deepcopy(default_conf)
conf['order_types']['sell'] = 'market'
with pytest.raises(OperationalException,
match='Market sell orders require ask_strategy.price_side = "bid".'):
validate_config_consistency(conf)
# Validate inversed case
conf = deepcopy(default_conf)
conf['order_types']['sell'] = 'market'
conf['order_types']['buy'] = 'market'
conf['ask_strategy']['price_side'] = 'bid'
conf['bid_strategy']['price_side'] = 'ask'
validate_config_consistency(conf)
def test_validate_tsl(default_conf):
default_conf['stoploss'] = 0.0
with pytest.raises(OperationalException, match='The config stoploss needs to be different '

View File

@ -94,6 +94,7 @@ def test_order_dict_dry_run(default_conf, mocker, caplog) -> None:
'stoploss': 'limit',
'stoploss_on_exchange': True,
}
conf['bid_strategy']['price_side'] = 'ask'
freqtrade = FreqtradeBot(conf)
assert freqtrade.strategy.order_types['stoploss_on_exchange']
@ -128,6 +129,7 @@ def test_order_dict_live(default_conf, mocker, caplog) -> None:
'stoploss': 'limit',
'stoploss_on_exchange': True,
}
conf['bid_strategy']['price_side'] = 'ask'
freqtrade = FreqtradeBot(conf)
assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog)
@ -837,17 +839,17 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None:
('ask', 4, 5, None, 0.5, 4), # last not available - uses ask
('ask', 4, 5, None, 1, 4), # last not available - uses ask
('ask', 4, 5, None, 0, 4), # last not available - uses ask
('bid', 10, 20, 10, 0.0, 20), # Full bid side
('bid', 10, 20, 10, 1.0, 10), # Full last side
('bid', 10, 20, 10, 0.5, 15), # Between bid and last
('bid', 10, 20, 10, 0.7, 13), # Between bid and last
('bid', 10, 20, 10, 0.3, 17), # Between bid and last
('bid', 4, 5, 10, 1.0, 5), # last bigger than bid
('bid', 4, 5, 10, 0.5, 5), # last bigger than bid
('bid', 10, 20, None, 0.5, 20), # last not available - uses bid
('bid', 4, 5, None, 0.5, 5), # last not available - uses bid
('bid', 4, 5, None, 1, 5), # last not available - uses bid
('bid', 4, 5, None, 0, 5), # last not available - uses bid
('bid', 21, 20, 10, 0.0, 20), # Full bid side
('bid', 21, 20, 10, 1.0, 10), # Full last side
('bid', 21, 20, 10, 0.5, 15), # Between bid and last
('bid', 21, 20, 10, 0.7, 13), # Between bid and last
('bid', 21, 20, 10, 0.3, 17), # Between bid and last
('bid', 6, 5, 10, 1.0, 5), # last bigger than bid
('bid', 6, 5, 10, 0.5, 5), # last bigger than bid
('bid', 21, 20, None, 0.5, 20), # last not available - uses bid
('bid', 6, 5, None, 0.5, 5), # last not available - uses bid
('bid', 6, 5, None, 1, 5), # last not available - uses bid
('bid', 6, 5, None, 0, 5), # last not available - uses bid
])
def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid,
last, last_ab, expected) -> None:
@ -856,7 +858,7 @@ def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid,
default_conf['bid_strategy']['price_side'] = side
freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
MagicMock(return_value={'ask': ask, 'last': last, 'bid': bid}))
return_value={'ask': ask, 'last': last, 'bid': bid})
assert freqtrade.get_buy_rate('ETH/BTC', True) == expected
assert not log_has("Using cached buy rate for ETH/BTC.", caplog)
@ -2243,6 +2245,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_
open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime
open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime
open_trade.close_profit_abs = 0.001
open_trade.is_open = False
Trade.session.add(open_trade)
@ -2290,6 +2293,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime
open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime
open_trade.close_profit_abs = 0.001
open_trade.is_open = False
Trade.session.add(open_trade)
@ -2794,7 +2798,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
side_effect=InvalidOrderException())
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=300))
sellmock = MagicMock()
sellmock = MagicMock(return_value={'id': '12345555'})
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
@ -4108,22 +4112,33 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_o
assert log_has('Sell Price at location 1 from orderbook could not be determined.', caplog)
@pytest.mark.parametrize('side,ask,bid,expected', [
('bid', 10.0, 11.0, 11.0),
('bid', 10.0, 11.2, 11.2),
('bid', 10.0, 11.0, 11.0),
('bid', 9.8, 11.0, 11.0),
('bid', 0.0001, 0.002, 0.002),
('ask', 10.0, 11.0, 10.0),
('ask', 10.11, 11.2, 10.11),
('ask', 0.001, 0.002, 0.001),
('ask', 0.006, 1.0, 0.006),
@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', [
('bid', 12.0, 11.0, 11.5, 0.0, 11.0), # full bid side
('bid', 12.0, 11.0, 11.5, 1.0, 11.5), # full last side
('bid', 12.0, 11.0, 11.5, 0.5, 11.25), # between bid and lat
('bid', 12.0, 11.2, 10.5, 0.0, 11.2), # Last smaller than bid
('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid
('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid
('bid', 0.003, 0.002, 0.005, 0.0, 0.002),
('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side
('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side
('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat
('ask', 12.2, 11.2, 10.5, 0.0, 12.2), # Last smaller than ask
('ask', 12.0, 11.0, 10.5, 1.0, 12.0), # Last smaller than ask - uses ask
('ask', 12.0, 11.2, 10.5, 0.5, 12.0), # Last smaller than ask - uses ask
('ask', 10.0, 11.0, 11.0, 0.0, 10.0),
('ask', 10.11, 11.2, 11.0, 0.0, 10.11),
('ask', 0.001, 0.002, 11.0, 0.0, 0.001),
('ask', 0.006, 1.0, 11.0, 0.0, 0.006),
])
def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask, expected) -> None:
def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask,
last, last_ab, expected) -> None:
caplog.set_level(logging.DEBUG)
default_conf['ask_strategy']['price_side'] = side
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'ask': ask, 'bid': bid})
default_conf['ask_strategy']['bid_last_balance'] = last_ab
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
return_value={'ask': ask, 'bid': bid, 'last': last})
pair = "ETH/BTC"
# Test regular mode
@ -4182,7 +4197,7 @@ def test_get_sell_rate_exception(default_conf, mocker, caplog):
default_conf['ask_strategy']['price_side'] = 'ask'
pair = "ETH/BTC"
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
return_value={'ask': None, 'bid': 0.12})
return_value={'ask': None, 'bid': 0.12, 'last': None})
ft = get_patched_freqtradebot(mocker, default_conf)
with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."):
ft.get_sell_rate(pair, True)
@ -4191,7 +4206,7 @@ def test_get_sell_rate_exception(default_conf, mocker, caplog):
assert ft.get_sell_rate(pair, True) == 0.12
# Reverse sides
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
return_value={'ask': 0.13, 'bid': None})
return_value={'ask': 0.13, 'bid': None, 'last': None})
with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."):
ft.get_sell_rate(pair, True)

View File

@ -1,5 +1,8 @@
# pragma pylint: disable=missing-docstring, C0103
import logging
from datetime import datetime, timedelta, timezone
from pathlib import Path
from types import FunctionType
from unittest.mock import MagicMock
import arrow
@ -8,7 +11,7 @@ from sqlalchemy import create_engine
from freqtrade import constants
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.persistence import Order, Trade, clean_dry_run_db, init_db
from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db
from tests.conftest import create_mock_trades, log_has, log_has_re
@ -19,14 +22,15 @@ def test_init_create_session(default_conf):
assert 'scoped_session' in type(Trade.session).__name__
def test_init_custom_db_url(default_conf, mocker):
def test_init_custom_db_url(default_conf, tmpdir):
# Update path to a value other than default, but still in-memory
default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock())
filename = f"{tmpdir}/freqtrade2_test.sqlite"
assert not Path(filename).is_file()
default_conf.update({'db_url': f'sqlite:///{filename}'})
init_db(default_conf['db_url'], default_conf['dry_run'])
assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
assert Path(filename).is_file()
def test_init_invalid_db_url(default_conf):
@ -47,15 +51,16 @@ def test_init_prod_db(default_conf, mocker):
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
def test_init_dryrun_db(default_conf, mocker):
default_conf.update({'dry_run': True})
default_conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock())
def test_init_dryrun_db(default_conf, tmpdir):
filename = f"{tmpdir}/freqtrade2_prod.sqlite"
assert not Path(filename).is_file()
default_conf.update({
'dry_run': True,
'db_url': f'sqlite:///{filename}'
})
init_db(default_conf['db_url'], default_conf['dry_run'])
assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.dryrun.sqlite'
assert Path(filename).is_file()
@pytest.mark.usefixtures("init_persistence")
@ -1039,14 +1044,45 @@ def test_fee_updated(fee):
@pytest.mark.usefixtures("init_persistence")
def test_total_open_trades_stakes(fee):
@pytest.mark.parametrize('use_db', [True, False])
def test_total_open_trades_stakes(fee, use_db):
Trade.use_db = use_db
Trade.reset_trades()
res = Trade.total_open_trades_stakes()
assert res == 0
create_mock_trades(fee)
create_mock_trades(fee, use_db)
res = Trade.total_open_trades_stakes()
assert res == 0.004
Trade.use_db = True
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize('use_db', [True, False])
def test_get_trades_proxy(fee, use_db):
Trade.use_db = use_db
Trade.reset_trades()
create_mock_trades(fee, use_db)
trades = Trade.get_trades_proxy()
assert len(trades) == 6
assert isinstance(trades[0], Trade)
trades = Trade.get_trades_proxy(is_open=True)
assert len(trades) == 4
assert trades[0].is_open
trades = Trade.get_trades_proxy(is_open=False)
assert len(trades) == 2
assert not trades[0].is_open
opendate = datetime.now(tz=timezone.utc) - timedelta(minutes=15)
assert len(Trade.get_trades_proxy(open_date=opendate)) == 3
Trade.use_db = True
@pytest.mark.usefixtures("init_persistence")
def test_get_overall_performance(fee):
@ -1172,3 +1208,25 @@ def test_select_order(fee):
assert order.ft_order_side == 'stoploss'
order = trades[4].select_order('sell', False)
assert order is None
def test_Trade_object_idem():
assert issubclass(Trade, LocalTrade)
trade = vars(Trade)
localtrade = vars(LocalTrade)
# Parent (LocalTrade) should have the same attributes
for item in trade:
# Exclude private attributes and open_date (as it's not assigned a default)
if (not item.startswith('_')
and item not in ('delete', 'session', 'query', 'open_date')):
assert item in localtrade
# Fails if only a column is added without corresponding parent field
for item in localtrade:
if (not item.startswith('__')
and item not in ('trades', 'trades_open', 'total_profit')
and type(getattr(LocalTrade, item)) not in (property, FunctionType)):
assert item in trade

View File

@ -3,6 +3,7 @@ import arrow
import pytest
from freqtrade.configuration import TimeRange
from freqtrade.exceptions import OperationalException
def test_parse_timerange_incorrect():
@ -27,9 +28,13 @@ def test_parse_timerange_incorrect():
timerange = TimeRange.parse_timerange('-1231006505000')
assert TimeRange(None, 'date', 0, 1231006505) == timerange
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
with pytest.raises(OperationalException, match=r'Incorrect syntax.*'):
TimeRange.parse_timerange('-')
with pytest.raises(OperationalException,
match=r'Start date is after stop date for timerange.*'):
TimeRange.parse_timerange('20100523-20100522')
def test_subtract_start():
x = TimeRange('date', 'date', 1274486400, 1438214400)