Merge pull request #6459 from freqtrade/new_release

New release 2022.2
This commit is contained in:
Matthias 2022-02-25 15:14:27 +01:00 committed by GitHub
commit 8f7b857ae9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 1573 additions and 578 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [xmatthias]

3
.gitignore vendored
View File

@ -10,6 +10,9 @@ freqtrade-plot.html
freqtrade-profit-plot.html freqtrade-profit-plot.html
freqtrade/rpc/api_server/ui/* freqtrade/rpc/api_server/ui/*
# Macos related
.DS_Store
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]

View File

@ -5,10 +5,14 @@
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io) [![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning. Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.
![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png) ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png)
## Sponsored promotion
[![tokenbot-promo](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/TokenBot-Freqtrade-banner.png)](https://tokenbot.com/?utm_source=github&utm_medium=freqtrade&utm_campaign=algodevs)
## Disclaimer ## Disclaimer
This software is for educational purposes only. Do not risk money which This software is for educational purposes only. Do not risk money which
@ -31,7 +35,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
- [X] [FTX](https://ftx.com) - [X] [FTX](https://ftx.com)
- [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [Kraken](https://kraken.com/) - [X] [Kraken](https://kraken.com/)
- [X] [OKEX](https://www.okex.com/) - [X] [OKX](https://www.okx.com/)
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ - [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
### Community tested ### Community tested
@ -57,9 +61,9 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/stable/edge/). - [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/stable/edge/).
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists. - [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists.
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid. - [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
- [x] **Builtin WebUI**: Builtin web UI to manage your bot.
- [x] **Manageable via Telegram**: Manage the bot with Telegram. - [x] **Manageable via Telegram**: Manage the bot with Telegram.
- [x] **Display profit/loss in fiat**: Display your profit/loss in 33 fiat. - [x] **Display profit/loss in fiat**: Display your profit/loss in fiat currency.
- [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss.
- [x] **Performance status report**: Provide a performance status of your current trades. - [x] **Performance status report**: Provide a performance status of your current trades.
## Quick start ## Quick start

View File

@ -8,6 +8,7 @@
"amend_last_stake_amount": false, "amend_last_stake_amount": false,
"last_stake_amount_min_ratio": 0.5, "last_stake_amount_min_ratio": 0.5,
"dry_run": true, "dry_run": true,
"dry_run_wallet": 1000,
"cancel_open_orders_on_exit": false, "cancel_open_orders_on_exit": false,
"timeframe": "5m", "timeframe": "5m",
"trailing_stop": false, "trailing_stop": false,
@ -86,6 +87,7 @@
"key": "your_exchange_key", "key": "your_exchange_key",
"secret": "your_exchange_secret", "secret": "your_exchange_secret",
"password": "", "password": "",
"log_responses": false,
"ccxt_config": {}, "ccxt_config": {},
"ccxt_async_config": {}, "ccxt_async_config": {},
"pair_whitelist": [ "pair_whitelist": [

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -313,6 +313,7 @@ A backtesting result will look like that:
| Avg. Duration Winners | 4:23:00 | | Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 | | Avg. Duration Loser | 6:55:00 |
| Rejected Buy signals | 3089 | | Rejected Buy signals | 3089 |
| Entry/Exit Timeouts | 0 / 0 |
| | | | | |
| Min balance | 0.00945123 BTC | | Min balance | 0.00945123 BTC |
| Max balance | 0.01846651 BTC | | Max balance | 0.01846651 BTC |
@ -400,6 +401,7 @@ It contains some useful key metrics about performance of your strategy on backte
| Avg. Duration Winners | 4:23:00 | | Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 | | Avg. Duration Loser | 6:55:00 |
| Rejected Buy signals | 3089 | | Rejected Buy signals | 3089 |
| Entry/Exit Timeouts | 0 / 0 |
| | | | | |
| Min balance | 0.00945123 BTC | | Min balance | 0.00945123 BTC |
| Max balance | 0.01846651 BTC | | Max balance | 0.01846651 BTC |
@ -429,6 +431,7 @@ It contains some useful key metrics about performance of your strategy on backte
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `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. - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
- `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached. - `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached.
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period. - `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
- `Drawdown (Account)`: Maximum Account Drawdown experienced. Calculated as $(Absolute Drawdown) / (DrawdownHigh + startingBalance)$. - `Drawdown (Account)`: Maximum Account Drawdown experienced. Calculated as $(Absolute Drawdown) / (DrawdownHigh + startingBalance)$.
- `Drawdown`: Maximum, absolute drawdown experienced. Difference between Drawdown High and Subsequent Low point. - `Drawdown`: Maximum, absolute drawdown experienced. Difference between Drawdown High and Subsequent Low point.

View File

@ -62,6 +62,7 @@ This loop will be repeated again and again until the bot is stopped.
* Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested. * Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested.
* Call `custom_stoploss()` and `custom_sell()` to find custom exit points. * Call `custom_stoploss()` and `custom_sell()` to find custom exit points.
* For sells based on sell-signal and custom-sell: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle). * For sells based on sell-signal and custom-sell: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_buy_timeout()` / `check_sell_timeout()` strategy callbacks.
* Generate backtest report output * Generate backtest report output
!!! Note !!! Note

View File

@ -182,13 +182,13 @@ Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force)
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues. For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore. Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore.
## OKEX ## OKX
OKEX requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows: OKX requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
```json ```json
"exchange": { "exchange": {
"name": "okex", "name": "okx",
"key": "your_exchange_key", "key": "your_exchange_key",
"secret": "your_exchange_secret", "secret": "your_exchange_secret",
"password": "your_exchange_api_key_password", "password": "your_exchange_api_key_password",
@ -197,7 +197,7 @@ OKEX requires a passphrase for each api key, you will therefore need to add this
``` ```
!!! Warning !!! Warning
OKEX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode. OKX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode.
## Gate.io ## Gate.io

View File

@ -116,7 +116,7 @@ optional arguments:
ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss,
SharpeHyperOptLoss, SharpeHyperOptLossDaily, SharpeHyperOptLoss, SharpeHyperOptLossDaily,
SortinoHyperOptLoss, SortinoHyperOptLossDaily, SortinoHyperOptLoss, SortinoHyperOptLossDaily,
CalmarHyperOptLoss, MaxDrawDownHyperOptLoss CalmarHyperOptLoss, MaxDrawDownHyperOptLoss, ProfitDrawDownHyperOptLoss
--disable-param-export --disable-param-export
Disable automatic hyperopt parameter export. Disable automatic hyperopt parameter export.
--ignore-missing-spaces, --ignore-unparameterized-spaces --ignore-missing-spaces, --ignore-unparameterized-spaces
@ -508,6 +508,46 @@ class MyAwesomeStrategy(IStrategy):
You will then obviously also change potential interesting entries to parameters to allow hyper-optimization. You will then obviously also change potential interesting entries to parameters to allow hyper-optimization.
### Optimizing `max_entry_position_adjustment`
While `max_entry_position_adjustment` is not a separate space, it can still be used in hyperopt by using the property approach shown above.
``` python
from pandas import DataFrame
from functools import reduce
import talib.abstract as ta
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
IStrategy, IntParameter)
import freqtrade.vendor.qtpylib.indicators as qtpylib
class MyAwesomeStrategy(IStrategy):
stoploss = -0.05
timeframe = '15m'
# Define the parameter spaces
max_epa = CategoricalParameter([-1, 0, 1, 3, 5, 10], default=1, space="buy", optimize=True)
@property
def max_entry_position_adjustment(self):
return self.max_epa.value
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# ...
```
??? Tip "Using `IntParameter`"
You can also use the `IntParameter` for this optimization, but you must explicitly return an integer:
``` python
max_epa = IntParameter(-1, 10, default=1, space="buy", optimize=True)
@property
def max_entry_position_adjustment(self):
return int(self.max_epa.value)
```
## Loss-functions ## Loss-functions
Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results.
@ -525,6 +565,7 @@ Currently, the following loss functions are builtin:
* `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation. * `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation.
* `MaxDrawDownHyperOptLoss` - Optimizes Maximum drawdown. * `MaxDrawDownHyperOptLoss` - Optimizes Maximum drawdown.
* `CalmarHyperOptLoss` - Optimizes Calmar Ratio calculated on trade returns relative to max drawdown. * `CalmarHyperOptLoss` - Optimizes Calmar Ratio calculated on trade returns relative to max drawdown.
* `ProfitDrawDownHyperOptLoss` - Optimizes by max Profit & min Drawdown objective. `DRAWDOWN_MULT` variable within the hyperoptloss file can be adjusted to be stricter or more flexible on drawdown purposes.
Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation. Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation.

View File

@ -246,7 +246,7 @@ On exchanges that deduct fees from the receiving currency (e.g. FTX) - this can
The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio. The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio.
This option is disabled by default, and will only apply if set to > 0. This option is disabled by default, and will only apply if set to > 0.
For `PriceFiler` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied. For `PriceFilter` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied.
Calculation example: Calculation example:

View File

@ -11,7 +11,7 @@
## Introduction ## Introduction
Freqtrade is a crypto-currency algorithmic trading software developed in python (3.8+) and supported on Windows, macOS and Linux. Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.
!!! Danger "DISCLAIMER" !!! Danger "DISCLAIMER"
This software is for educational purposes only. Do not risk money which you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS. This software is for educational purposes only. Do not risk money which you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
@ -20,6 +20,12 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python
We strongly recommend you to have basic coding skills and Python knowledge. Do not hesitate to read the source code and understand the mechanisms of this bot, algorithms and techniques implemented in it. We strongly recommend you to have basic coding skills and Python knowledge. Do not hesitate to read the source code and understand the mechanisms of this bot, algorithms and techniques implemented in it.
![freqtrade screenshot](assets/freqtrade-screenshot.png)
## Sponsored promotion
[![tokenbot-promo](assets/TokenBot-Freqtrade-banner.png)](https://tokenbot.com/?utm_source=github&utm_medium=freqtrade&utm_campaign=algodevs)
## Features ## Features
- Develop your Strategy: Write your strategy in python, using [pandas](https://pandas.pydata.org/). Example strategies to inspire you are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies). - Develop your Strategy: Write your strategy in python, using [pandas](https://pandas.pydata.org/). Example strategies to inspire you are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies).
@ -29,7 +35,7 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python
- Select markets: Create your static list or use an automatic one based on top traded volumes and/or prices (not available during backtesting). You can also explicitly blacklist markets you don't want to trade. - Select markets: Create your static list or use an automatic one based on top traded volumes and/or prices (not available during backtesting). You can also explicitly blacklist markets you don't want to trade.
- Run: Test your strategy with simulated money (Dry-Run mode) or deploy it with real money (Live-Trade mode). - Run: Test your strategy with simulated money (Dry-Run mode) or deploy it with real money (Live-Trade mode).
- Run using Edge (optional module): The concept is to find the best historical [trade expectancy](edge.md#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade. The sizing of the trade is based on a risk of a percentage of your capital. - Run using Edge (optional module): The concept is to find the best historical [trade expectancy](edge.md#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade. The sizing of the trade is based on a risk of a percentage of your capital.
- Control/Monitor: Use Telegram or a REST API (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.). - Control/Monitor: Use Telegram or a WebUI (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.).
- Analyse: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md). - Analyse: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md).
## Supported exchange marketplaces ## Supported exchange marketplaces
@ -41,7 +47,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
- [X] [FTX](https://ftx.com) - [X] [FTX](https://ftx.com)
- [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [Kraken](https://kraken.com/) - [X] [Kraken](https://kraken.com/)
- [X] [OKEX](https://www.okex.com/) - [X] [OKX](https://www.okx.com/)
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ - [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
### Community tested ### Community tested

View File

@ -24,7 +24,7 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable).
!!! Note !!! Note
Python3.7 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. Python3.8 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
Also, python headers (`python<yourversion>-dev` / `python<yourversion>-devel`) must be available for the installation to complete successfully. Also, python headers (`python<yourversion>-dev` / `python<yourversion>-devel`) must be available for the installation to complete successfully.
!!! Warning "Up-to-date clock" !!! Warning "Up-to-date clock"
@ -54,7 +54,7 @@ We've included/collected install instructions for Ubuntu, MacOS, and Windows. Th
OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems.
!!! Note !!! Note
Python3.7 or higher and the corresponding pip are assumed to be available. Python3.8 or higher and the corresponding pip are assumed to be available.
=== "Debian/Ubuntu" === "Debian/Ubuntu"
#### Install necessary dependencies #### Install necessary dependencies
@ -69,7 +69,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces
=== "RaspberryPi/Raspbian" === "RaspberryPi/Raspbian"
The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/). The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/).
This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running. This image comes with python3.9 preinstalled, making it easy to get freqtrade up and running.
Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied. Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied.
@ -169,7 +169,7 @@ You can as well update, configure and reset the codebase of your bot with `./scr
** --install ** ** --install **
With this option, the script will install the bot and most dependencies: With this option, the script will install the bot and most dependencies:
You will need to have git and python3.7+ installed beforehand for this to work. You will need to have git and python3.8+ installed beforehand for this to work.
* Mandatory software as: `ta-lib` * Mandatory software as: `ta-lib`
* Setup your virtualenv under `.env/` * Setup your virtualenv under `.env/`

View File

@ -318,8 +318,8 @@ optional arguments:
Specify what timerange of data to use. Specify what timerange of data to use.
--export EXPORT Export backtest results, argument are: trades. --export EXPORT Export backtest results, argument are: trades.
Example: `--export=trades` Example: `--export=trades`
--export-filename PATH --export-filename PATH, --backtest-filename PATH
Save backtest results to the file with this filename. Use backtest results from this filename.
Requires `--export` to be set as well. Example: Requires `--export` to be set as well. Example:
`--export-filename=user_data/backtest_results/backtest `--export-filename=user_data/backtest_results/backtest
_today.json` _today.json`

View File

@ -1,4 +1,4 @@
mkdocs==1.2.3 mkdocs==1.2.3
mkdocs-material==8.1.9 mkdocs-material==8.2.1
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2
pymdown-extensions==9.1 pymdown-extensions==9.2

View File

@ -2,6 +2,7 @@
The `stoploss` configuration parameter is loss as ratio that should trigger a sale. The `stoploss` configuration parameter is loss as ratio that should trigger a sale.
For example, value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional. For example, value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional.
Stoploss calculations do include fees, so a stoploss of -10% is placed exactly 10% below the entry point.
Most of the strategy files already include the optimal `stoploss` value. Most of the strategy files already include the optimal `stoploss` value.
@ -30,7 +31,7 @@ These modes can be configured with these values:
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio ### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
Enable or Disable stop loss on exchange. Enable or Disable stop loss on exchange.
If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order fills. This will protect you against sudden crashes in market, as the order execution happens purely within the exchange, and has no potential network overhead.
If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price. If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price.
`stoploss` defines the stop-price where the limit order is placed - and limit should be slightly below this. `stoploss` defines the stop-price where the limit order is placed - and limit should be slightly below this.

View File

@ -389,8 +389,8 @@ class AwesomeStrategy(IStrategy):
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98, which is 2% below the current (proposed) rate. If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98, which is 2% below the current (proposed) rate.
!!! Warning "Backtesting" !!! Warning "Backtesting"
While Custom prices are supported in backtesting (starting with 2021.12), prices will be moved to within the candle's high/low prices. Custom prices are supported in backtesting (starting with 2021.12), and orders will fill if the price falls within the candle's low/high range.
This behavior is currently being tested, and might be changed at a later point. Orders that don't fill immediately are subject to regular timeout handling, which happens once per (detail) candle.
`custom_exit_price()` is only called for sells of type Sell_signal and Custom sell. All other sell-types will use regular backtesting prices. `custom_exit_price()` is only called for sells of type Sell_signal and Custom sell. All other sell-types will use regular backtesting prices.
## Custom order timeout rules ## Custom order timeout rules
@ -400,7 +400,8 @@ Simple, time-based order-timeouts can be configured either via strategy or in th
However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if an order did time out or not. However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if an order did time out or not.
!!! Note !!! Note
Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances. Backtesting fills orders if their price falls within the candle's low/high range.
The below callbacks will be called once per (detail) candle for orders that don't fill immediately (which use custom pricing).
### Custom order timeout example ### Custom order timeout example
@ -467,7 +468,8 @@ class AwesomeStrategy(IStrategy):
'sell': 60 * 25 'sell': 60 * 25
} }
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: def check_buy_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool:
ob = self.dp.orderbook(pair, 1) ob = self.dp.orderbook(pair, 1)
current_price = ob['bids'][0][0] current_price = ob['bids'][0][0]
# Cancel buy order if price is more than 2% above the order. # Cancel buy order if price is more than 2% above the order.
@ -476,7 +478,8 @@ class AwesomeStrategy(IStrategy):
return False return False
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: def check_sell_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool:
ob = self.dp.orderbook(pair, 1) ob = self.dp.orderbook(pair, 1)
current_price = ob['asks'][0][0] current_price = ob['asks'][0][0]
# Cancel sell order if price is more than 2% below the order. # Cancel sell order if price is more than 2% below the order.

View File

@ -1,5 +1,5 @@
""" Freqtrade bot """ """ Freqtrade bot """
__version__ = '2022.1' __version__ = '2022.2'
if __version__ == 'develop': if __version__ == 'develop':

View File

@ -75,7 +75,7 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
"timerange", "timeframe", "no_trades"] "timerange", "timeframe", "no_trades"]
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
"trade_source", "timeframe", "plot_auto_open"] "trade_source", "timeframe", "plot_auto_open", ]
ARGS_INSTALL_UI = ["erase_ui_only", 'ui_version'] ARGS_INSTALL_UI = ["erase_ui_only", 'ui_version']

View File

@ -112,7 +112,7 @@ def ask_user_config() -> Dict[str, Any]:
"ftx", "ftx",
"kucoin", "kucoin",
"gateio", "gateio",
"okex", "okx",
Separator(), Separator(),
"other", "other",
], ],
@ -140,7 +140,7 @@ def ask_user_config() -> Dict[str, Any]:
"type": "password", "type": "password",
"name": "exchange_key_password", "name": "exchange_key_password",
"message": "Insert Exchange API Key password", "message": "Insert Exchange API Key password",
"when": lambda x: not x['dry_run'] and x['exchange_name'] in ('kucoin', 'okex') "when": lambda x: not x['dry_run'] and x['exchange_name'] in ('kucoin', 'okx')
}, },
{ {
"type": "confirm", "type": "confirm",

View File

@ -182,11 +182,12 @@ AVAILABLE_CLI_OPTIONS = {
), ),
"exportfilename": Arg( "exportfilename": Arg(
'--export-filename', "--export-filename",
help='Save backtest results to the file with this filename. ' "--backtest-filename",
'Requires `--export` to be set as well. ' help="Use this filename for backtest results."
'Example: `--export-filename=user_data/backtest_results/backtest_today.json`', "Requires `--export` to be set as well. "
metavar='PATH', "Example: `--export-filename=user_data/backtest_results/backtest_today.json`",
metavar="PATH",
), ),
"disableparamexport": Arg( "disableparamexport": Arg(
'--disable-param-export', '--disable-param-export',

View File

@ -431,7 +431,6 @@ class Configuration:
logstring='Using "{}" to store trades data.') logstring='Using "{}" to store trades data.')
def _process_data_options(self, config: Dict[str, Any]) -> None: def _process_data_options(self, config: Dict[str, Any]) -> None:
self._args_to_config(config, argname='new_pairs_days', self._args_to_config(config, argname='new_pairs_days',
logstring='Detected --new-pairs-days: {}') logstring='Detected --new-pairs-days: {}')

View File

@ -26,7 +26,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
'CalmarHyperOptLoss', 'CalmarHyperOptLoss',
'MaxDrawDownHyperOptLoss'] 'MaxDrawDownHyperOptLoss', 'ProfitDrawDownHyperOptLoss']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
'AgeFilter', 'OffsetFilter', 'PerformanceFilter', 'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
@ -456,6 +456,7 @@ SCHEMA_BACKTEST_REQUIRED = [
'dry_run_wallet', 'dry_run_wallet',
'dataformat_ohlcv', 'dataformat_ohlcv',
'dataformat_trades', 'dataformat_trades',
'unfilledtimeout',
] ]
SCHEMA_MINIMAL_REQUIRED = [ SCHEMA_MINIMAL_REQUIRED = [

View File

@ -20,4 +20,4 @@ from freqtrade.exchange.gateio import Gateio
from freqtrade.exchange.hitbtc import Hitbtc from freqtrade.exchange.hitbtc import Hitbtc
from freqtrade.exchange.kraken import Kraken from freqtrade.exchange.kraken import Kraken
from freqtrade.exchange.kucoin import Kucoin from freqtrade.exchange.kucoin import Kucoin
from freqtrade.exchange.okex import Okex from freqtrade.exchange.okx import Okx

View File

@ -27,13 +27,15 @@ API_FETCH_ORDER_RETRY_COUNT = 5
BAD_EXCHANGES = { BAD_EXCHANGES = {
"bitmex": "Various reasons.", "bitmex": "Various reasons.",
"phemex": "Does not provide history. ", "phemex": "Does not provide history.",
"probit": "Requires additional, regular calls to `signIn()`.",
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.", "poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.",
} }
MAP_EXCHANGE_CHILDCLASS = { MAP_EXCHANGE_CHILDCLASS = {
'binanceus': 'binance', 'binanceus': 'binance',
'binanceje': 'binance', 'binanceje': 'binance',
'okex': 'okx',
} }

View File

@ -1587,7 +1587,7 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non
def is_exchange_officially_supported(exchange_name: str) -> bool: def is_exchange_officially_supported(exchange_name: str) -> bool:
return exchange_name in ['bittrex', 'binance', 'kraken', 'ftx', 'gateio', 'okex'] return exchange_name in ['bittrex', 'binance', 'kraken', 'ftx', 'gateio', 'okx']
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]: def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:

View File

@ -106,7 +106,9 @@ class Ftx(Exchange):
if order[0].get('status') == 'closed': if order[0].get('status') == 'closed':
# Trigger order was triggered ... # Trigger order was triggered ...
real_order_id = order[0].get('info', {}).get('orderId') real_order_id = order[0].get('info', {}).get('orderId')
# OrderId may be None for stoploss-market orders
# But contains "average" in these cases.
if real_order_id:
order1 = self._api.fetch_order(real_order_id, pair) order1 = self._api.fetch_order(real_order_id, pair)
self._log_exchange_response('fetch_stoploss_order1', order1) self._log_exchange_response('fetch_stoploss_order1', order1)
# Fake type to stop - as this was really a stop order. # Fake type to stop - as this was really a stop order.
@ -115,6 +117,7 @@ class Ftx(Exchange):
order1['type'] = 'stop' order1['type'] = 'stop'
order1['status_stop'] = 'triggered' order1['status_stop'] = 'triggered'
return order1 return order1
return order[0] return order[0]
else: else:
raise InvalidOrderException(f"Could not get stoploss order for id {order_id}") raise InvalidOrderException(f"Could not get stoploss order for id {order_id}")

View File

@ -7,8 +7,8 @@ from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Okex(Exchange): class Okx(Exchange):
"""Okex exchange class. """Okx exchange class.
Contains adjustments needed for Freqtrade to work with this exchange. Contains adjustments needed for Freqtrade to work with this exchange.
""" """

View File

@ -100,6 +100,8 @@ class FreqtradeBot(LoggingMixin):
self._exit_lock = Lock() self._exit_lock = Lock()
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc)
def notify_status(self, msg: str) -> None: def notify_status(self, msg: str) -> None:
""" """
Public method for users of this class (worker, etc.) to send notifications Public method for users of this class (worker, etc.) to send notifications
@ -187,6 +189,7 @@ class FreqtradeBot(LoggingMixin):
self.enter_positions() self.enter_positions()
Trade.commit() Trade.commit()
self.last_process = datetime.now(timezone.utc)
def process_stopped(self) -> None: def process_stopped(self) -> None:
""" """
@ -295,28 +298,6 @@ class FreqtradeBot(LoggingMixin):
self.update_trade_state(trade, order.order_id, send_msg=False) self.update_trade_state(trade, order.order_id, send_msg=False)
def handle_insufficient_funds(self, trade: Trade): def handle_insufficient_funds(self, trade: Trade):
"""
Determine if we ever opened a sell order for this trade.
If not, try update buy fees - otherwise "refind" the open order we obviously lost.
"""
sell_order = trade.select_order('sell', None)
if sell_order:
self.refind_lost_order(trade)
else:
self.reupdate_enter_order_fees(trade)
def reupdate_enter_order_fees(self, trade: Trade):
"""
Get buy order from database, and try to reupdate.
Handles trades where the initial fee-update did not work.
"""
logger.info(f"Trying to reupdate buy fees for {trade}")
order = trade.select_order('buy', False)
if order:
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
self.update_trade_state(trade, order.order_id, send_msg=False)
def refind_lost_order(self, trade):
""" """
Try refinding a lost trade. Try refinding a lost trade.
Only used when InsufficientFunds appears on sell orders (stoploss or sell). Only used when InsufficientFunds appears on sell orders (stoploss or sell).
@ -329,9 +310,6 @@ class FreqtradeBot(LoggingMixin):
if not order.ft_is_open: if not order.ft_is_open:
logger.debug(f"Order {order} is no longer open.") logger.debug(f"Order {order} is no longer open.")
continue continue
if order.ft_order_side == 'buy':
# Skip buy side - this is handled by reupdate_buy_order_fees
continue
try: try:
fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
order.ft_order_side == 'stoploss') order.ft_order_side == 'stoploss')
@ -343,6 +321,9 @@ class FreqtradeBot(LoggingMixin):
if fo and fo['status'] == 'open': if fo and fo['status'] == 'open':
# Assume this as the open order # Assume this as the open order
trade.open_order_id = order.order_id trade.open_order_id = order.order_id
elif order.ft_order_side == 'buy':
if fo and fo['status'] == 'open':
trade.open_order_id = order.order_id
if fo: if fo:
logger.info(f"Found {order} for trade {trade}.") logger.info(f"Found {order} for trade {trade}.")
self.update_trade_state(trade, order.order_id, fo, self.update_trade_state(trade, order.order_id, fo,
@ -984,18 +965,20 @@ class FreqtradeBot(LoggingMixin):
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
order_obj = trade.select_order_by_order_id(trade.open_order_id)
if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and ( if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and (
fully_cancelled fully_cancelled
or self.strategy.ft_check_timed_out( or (order_obj and self.strategy.ft_check_timed_out(
'buy', trade, order, datetime.now(timezone.utc)) 'buy', trade, order_obj, datetime.now(timezone.utc))
)): ))):
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and ( elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
fully_cancelled fully_cancelled
or self.strategy.ft_check_timed_out( or (order_obj and self.strategy.ft_check_timed_out(
'sell', trade, order, datetime.now(timezone.utc))) 'sell', trade, order_obj, datetime.now(timezone.utc))
): ))):
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
canceled_count = trade.get_exit_order_count() canceled_count = trade.get_exit_order_count()
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
@ -1038,12 +1021,12 @@ class FreqtradeBot(LoggingMixin):
# Cancelled orders may have the status of 'canceled' or 'closed' # Cancelled orders may have the status of 'canceled' or 'closed'
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES: if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
filled_val = order.get('filled', 0.0) or 0.0 filled_val: float = order.get('filled', 0.0) or 0.0
filled_stake = filled_val * trade.open_rate filled_stake = filled_val * trade.open_rate
minstake = self.exchange.get_min_pair_stake_amount( minstake = self.exchange.get_min_pair_stake_amount(
trade.pair, trade.open_rate, self.strategy.stoploss) trade.pair, trade.open_rate, self.strategy.stoploss)
if filled_val > 0 and filled_stake < minstake: if filled_val > 0 and minstake and filled_stake < minstake:
logger.warning( logger.warning(
f"Order {trade.open_order_id} for {trade.pair} not cancelled, " f"Order {trade.open_order_id} for {trade.pair} not cancelled, "
f"as the filled amount of {filled_val} would result in an unsellable trade.") f"as the filled amount of {filled_val} would result in an unsellable trade.")
@ -1374,9 +1357,14 @@ class FreqtradeBot(LoggingMixin):
# Handling of this will happen in check_handle_timedout. # Handling of this will happen in check_handle_timedout.
return True return True
order = self.handle_order_fee(trade, order) order_obj = trade.select_order_by_order_id(order_id)
if not order_obj:
raise DependencyException(
f"Order_obj not found for {order_id}. This should not have happened.")
self.handle_order_fee(trade, order_obj, order)
trade.update(order) trade.update_trade(order_obj)
# TODO: is the below necessary? it's already done in update_trade for filled buys
trade.recalc_trade_from_orders() trade.recalc_trade_from_orders()
Trade.commit() Trade.commit()
@ -1428,17 +1416,15 @@ class FreqtradeBot(LoggingMixin):
return real_amount return real_amount
return amount return amount
def handle_order_fee(self, trade: Trade, order: Dict[str, Any]) -> Dict[str, Any]: def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> None:
# Try update amount (binance-fix) # Try update amount (binance-fix)
try: try:
new_amount = self.get_real_amount(trade, order) new_amount = self.get_real_amount(trade, order)
if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount, if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount,
abs_tol=constants.MATH_CLOSE_PREC): abs_tol=constants.MATH_CLOSE_PREC):
order['amount'] = new_amount order_obj.ft_fee_base = trade.amount - new_amount
order.pop('filled', None)
except DependencyException as exception: except DependencyException as exception:
logger.warning("Could not update trade amount: %s", exception) logger.warning("Could not update trade amount: %s", exception)
return order
def get_real_amount(self, trade: Trade, order: Dict) -> float: def get_real_amount(self, trade: Trade, order: Dict) -> float:
""" """

View File

@ -29,18 +29,23 @@ def decimals_per_coin(coin: str):
return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK) return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK)
def round_coin_value(value: float, coin: str, show_coin_name=True) -> str: def round_coin_value(
value: float, coin: str, show_coin_name=True, keep_trailing_zeros=False) -> str:
""" """
Get price value for this coin Get price value for this coin
:param value: Value to be printed :param value: Value to be printed
:param coin: Which coin are we printing the price / value for :param coin: Which coin are we printing the price / value for
:param show_coin_name: Return string in format: "222.22 USDT" or "222.22" :param show_coin_name: Return string in format: "222.22 USDT" or "222.22"
:param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2"
:return: Formatted / rounded value (with or without coin name) :return: Formatted / rounded value (with or without coin name)
""" """
val = f"{value:.{decimals_per_coin(coin)}f}"
if not keep_trailing_zeros:
val = val.rstrip('0').rstrip('.')
if show_coin_name: if show_coin_name:
return f"{value:.{decimals_per_coin(coin)}f} {coin}" val = f"{val} {coin}"
else:
return f"{value:.{decimals_per_coin(coin)}f}" return val
def shorten_date(_date: str) -> str: def shorten_date(_date: str) -> str:

View File

@ -63,6 +63,8 @@ class Backtesting:
LoggingMixin.show_output = False LoggingMixin.show_output = False
self.config = config self.config = config
self.results: Dict[str, Any] = {} self.results: Dict[str, Any] = {}
self.trade_id_counter: int = 0
self.order_id_counter: int = 0
config['dry_run'] = True config['dry_run'] = True
self.run_ids: Dict[str, str] = {} self.run_ids: Dict[str, str] = {}
@ -126,7 +128,8 @@ class Backtesting:
def __del__(self): def __del__(self):
self.cleanup() self.cleanup()
def cleanup(self): @staticmethod
def cleanup():
LoggingMixin.show_output = True LoggingMixin.show_output = True
PairLocks.use_db = True PairLocks.use_db = True
Trade.use_db = True Trade.use_db = True
@ -231,6 +234,8 @@ class Backtesting:
PairLocks.reset_locks() PairLocks.reset_locks()
Trade.reset_trades() Trade.reset_trades()
self.rejected_trades = 0 self.rejected_trades = 0
self.timedout_entry_orders = 0
self.timedout_exit_orders = 0
self.dataprovider.clear_cache() self.dataprovider.clear_cache()
if enable_protections: if enable_protections:
self._load_protections(self.strategy) self._load_protections(self.strategy)
@ -275,6 +280,13 @@ class Backtesting:
# Trim startup period from analyzed dataframe # Trim startup period from analyzed dataframe
df_analyzed = processed[pair] = pair_data = trim_dataframe( df_analyzed = processed[pair] = pair_data = trim_dataframe(
df_analyzed, self.timerange, startup_candles=self.required_startup) df_analyzed, self.timerange, startup_candles=self.required_startup)
# Update dataprovider cache
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
# Create a copy of the dataframe before shifting, that way the buy signal/tag
# remains on the correct candle for callbacks.
df_analyzed = df_analyzed.copy()
# To avoid using data from future, we use buy/sell signals shifted # To avoid using data from future, we use buy/sell signals shifted
# from the previous candle # from the previous candle
df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1) df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1)
@ -282,9 +294,6 @@ class Backtesting:
df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1) df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1)
df_analyzed.loc[:, 'exit_tag'] = df_analyzed.loc[:, 'exit_tag'].shift(1) df_analyzed.loc[:, 'exit_tag'] = df_analyzed.loc[:, 'exit_tag'].shift(1)
# Update dataprovider cache
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
df_analyzed = df_analyzed.drop(df_analyzed.head(1).index) df_analyzed = df_analyzed.drop(df_analyzed.head(1).index)
# Convert from Pandas to list for performance reasons # Convert from Pandas to list for performance reasons
@ -349,7 +358,22 @@ class Backtesting:
# use Open rate if open_rate > calculated sell rate # use Open rate if open_rate > calculated sell rate
return sell_row[OPEN_IDX] return sell_row[OPEN_IDX]
return close_rate if (
trade_dur == 0
# Red candle (for longs), TODO: green candle (for shorts)
and sell_row[OPEN_IDX] > sell_row[CLOSE_IDX] # Red candle
and trade.open_rate < sell_row[OPEN_IDX] # trade-open below open_rate
and close_rate > sell_row[CLOSE_IDX]
):
# ROI on opening candles with custom pricing can only
# trigger if the entry was at Open or lower.
# details: https: // github.com/freqtrade/freqtrade/issues/6261
# If open_rate is < open, only allow sells below the close on red candles.
raise ValueError("Opening candle ROI on red candles.")
# Use the maximum between close_rate and low as we
# cannot sell outside of a candle.
# Applies when a new ROI setting comes in place and the whole candle is above that.
return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX])
else: else:
# This should not be reached... # This should not be reached...
@ -372,10 +396,15 @@ class Backtesting:
if stake_amount is not None and stake_amount > 0.0: if stake_amount is not None and stake_amount > 0.0:
pos_trade = self._enter_trade(trade.pair, row, stake_amount, trade) pos_trade = self._enter_trade(trade.pair, row, stake_amount, trade)
if pos_trade is not None: if pos_trade is not None:
self.wallets.update()
return pos_trade return pos_trade
return trade return trade
def _get_order_filled(self, rate: float, row: Tuple) -> bool:
""" Rate is within candle, therefore filled"""
return row[LOW_IDX] <= rate <= row[HIGH_IDX]
def _get_sell_trade_entry_for_candle(self, trade: LocalTrade, def _get_sell_trade_entry_for_candle(self, trade: LocalTrade,
sell_row: Tuple) -> Optional[LocalTrade]: sell_row: Tuple) -> Optional[LocalTrade]:
@ -398,21 +427,27 @@ class Backtesting:
trade.close_date = sell_candle_time trade.close_date = sell_candle_time
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
try:
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
except ValueError:
return None
# call the custom exit price,with default value as previous closerate # call the custom exit price,with default value as previous closerate
current_profit = trade.calc_profit_ratio(closerate) current_profit = trade.calc_profit_ratio(closerate)
order_type = self.strategy.order_types['sell']
if sell.sell_type in (SellType.SELL_SIGNAL, SellType.CUSTOM_SELL): if sell.sell_type in (SellType.SELL_SIGNAL, SellType.CUSTOM_SELL):
# Custom exit pricing only for sell-signals # Custom exit pricing only for sell-signals
if order_type == 'limit':
closerate = strategy_safe_wrapper(self.strategy.custom_exit_price, closerate = strategy_safe_wrapper(self.strategy.custom_exit_price,
default_retval=closerate)( default_retval=closerate)(
pair=trade.pair, trade=trade, pair=trade.pair, trade=trade,
current_time=sell_row[DATE_IDX], current_time=sell_candle_time,
proposed_rate=closerate, current_profit=current_profit) proposed_rate=closerate, current_profit=current_profit)
# Use the maximum between close_rate and low as we cannot sell outside of a candle. # We can't place orders lower than current low.
closerate = min(max(closerate, sell_row[LOW_IDX]), sell_row[HIGH_IDX]) # freqtrade does not support this in live, and the order would fill immediately
closerate = max(closerate, sell_row[LOW_IDX])
# Confirm trade exit: # Confirm trade exit:
time_in_force = self.strategy.order_time_in_force['sell'] time_in_force = self.strategy.order_time_in_force['sell']
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
rate=closerate, rate=closerate,
@ -432,7 +467,28 @@ class Backtesting:
): ):
trade.sell_reason = sell_row[EXIT_TAG_IDX] trade.sell_reason = sell_row[EXIT_TAG_IDX]
trade.close(closerate, show_msg=False) self.order_id_counter += 1
order = Order(
id=self.order_id_counter,
ft_trade_id=trade.id,
order_date=sell_candle_time,
order_update_date=sell_candle_time,
ft_is_open=True,
ft_pair=trade.pair,
order_id=str(self.order_id_counter),
symbol=trade.pair,
ft_order_side="sell",
side="sell",
order_type=order_type,
status="open",
price=closerate,
average=closerate,
amount=trade.amount,
filled=0,
remaining=trade.amount,
cost=trade.amount * closerate,
)
trade.orders.append(order)
return trade return trade
return None return None
@ -471,13 +527,16 @@ class Backtesting:
current_time = row[DATE_IDX].to_pydatetime() current_time = row[DATE_IDX].to_pydatetime()
entry_tag = row[BUY_TAG_IDX] if len(row) >= BUY_TAG_IDX + 1 else None entry_tag = row[BUY_TAG_IDX] if len(row) >= BUY_TAG_IDX + 1 else None
# let's call the custom entry price, using the open price as default price # let's call the custom entry price, using the open price as default price
order_type = self.strategy.order_types['buy']
propose_rate = row[OPEN_IDX]
if order_type == 'limit':
propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price, propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=row[OPEN_IDX])( default_retval=row[OPEN_IDX])(
pair=pair, current_time=current_time, pair=pair, current_time=current_time,
proposed_rate=row[OPEN_IDX], entry_tag=entry_tag) # default value is the open rate proposed_rate=propose_rate, entry_tag=entry_tag) # default value is the open rate
# We can't place orders higher than current high (otherwise it'd be a stop limit buy)
# Move rate to within the candle's low/high rate # which freqtrade does not support in live.
propose_rate = min(max(propose_rate, row[LOW_IDX]), row[HIGH_IDX]) propose_rate = min(propose_rate, row[HIGH_IDX])
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0 min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0
max_stake_amount = self.wallets.get_available_stake_amount() max_stake_amount = self.wallets.get_available_stake_amount()
@ -485,9 +544,9 @@ class Backtesting:
pos_adjust = trade is not None pos_adjust = trade is not None
if not pos_adjust: if not pos_adjust:
try: try:
stake_amount = self.wallets.get_trade_stake_amount(pair, None) stake_amount = self.wallets.get_trade_stake_amount(pair, None, update=False)
except DependencyException: except DependencyException:
return trade return None
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
default_retval=stake_amount)( default_retval=stake_amount)(
@ -502,8 +561,7 @@ class Backtesting:
# If not pos adjust, trade is None # If not pos adjust, trade is None
return trade return trade
order_type = self.strategy.order_types['buy'] time_in_force = self.strategy.order_time_in_force['buy']
time_in_force = self.strategy.order_time_in_force['sell']
# Confirm trade entry: # Confirm trade entry:
if not pos_adjust: if not pos_adjust:
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
@ -513,15 +571,21 @@ class Backtesting:
return None return None
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
self.order_id_counter += 1
amount = round(stake_amount / propose_rate, 8) amount = round(stake_amount / propose_rate, 8)
if trade is None: if trade is None:
# Enter trade # Enter trade
self.trade_id_counter += 1
trade = LocalTrade( trade = LocalTrade(
id=self.trade_id_counter,
open_order_id=self.order_id_counter,
pair=pair, pair=pair,
open_rate=propose_rate, open_rate=propose_rate,
open_rate_requested=propose_rate,
open_date=current_time, open_date=current_time,
stake_amount=stake_amount, stake_amount=stake_amount,
amount=amount, amount=amount,
amount_requested=amount,
fee_open=self.fee, fee_open=self.fee,
fee_close=self.fee, fee_close=self.fee,
is_open=True, is_open=True,
@ -529,27 +593,35 @@ class Backtesting:
exchange='backtesting', exchange='backtesting',
orders=[] orders=[]
) )
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
order = Order( order = Order(
ft_is_open=False, id=self.order_id_counter,
ft_trade_id=trade.id,
ft_is_open=True,
ft_pair=trade.pair, ft_pair=trade.pair,
order_id=str(self.order_id_counter),
symbol=trade.pair, symbol=trade.pair,
ft_order_side="buy", ft_order_side="buy",
side="buy", side="buy",
order_type="market", order_type=order_type,
status="closed", status="open",
order_date=current_time, order_date=current_time,
order_filled_date=current_time, order_filled_date=current_time,
order_update_date=current_time, order_update_date=current_time,
price=propose_rate, price=propose_rate,
average=propose_rate, average=propose_rate,
amount=amount, amount=amount,
filled=amount, filled=0,
cost=stake_amount + trade.fee_open remaining=amount,
cost=stake_amount + trade.fee_open,
) )
if pos_adjust and self._get_order_filled(order.price, row):
order.close_bt_order(current_time)
else:
trade.open_order_id = str(self.order_id_counter)
trade.orders.append(order) trade.orders.append(order)
if pos_adjust:
trade.recalc_trade_from_orders() trade.recalc_trade_from_orders()
return trade return trade
@ -563,6 +635,9 @@ class Backtesting:
for pair in open_trades.keys(): for pair in open_trades.keys():
if len(open_trades[pair]) > 0: if len(open_trades[pair]) > 0:
for trade in open_trades[pair]: for trade in open_trades[pair]:
if trade.open_order_id and trade.nr_of_successful_buys == 0:
# Ignore trade if buy-order did not fill yet
continue
sell_row = data[pair][-1] sell_row = data[pair][-1]
trade.close_date = sell_row[DATE_IDX].to_pydatetime() trade.close_date = sell_row[DATE_IDX].to_pydatetime()
@ -583,6 +658,51 @@ class Backtesting:
self.rejected_trades += 1 self.rejected_trades += 1
return False return False
def run_protections(self, enable_protections, pair: str, current_time: datetime):
if enable_protections:
self.protections.stop_per_pair(pair, current_time)
self.protections.global_stop(current_time)
def check_order_cancel(self, trade: LocalTrade, current_time) -> bool:
"""
Check if an order has been canceled.
Returns True if the trade should be Deleted (initial order was canceled).
"""
for order in [o for o in trade.orders if o.ft_is_open]:
timedout = self.strategy.ft_check_timed_out(order.side, trade, order, current_time)
if timedout:
if order.side == 'buy':
self.timedout_entry_orders += 1
if trade.nr_of_successful_buys == 0:
# Remove trade due to buy timeout expiration.
return True
else:
# Close additional buy order
del trade.orders[trade.orders.index(order)]
if order.side == 'sell':
self.timedout_exit_orders += 1
# Close sell order and retry selling on next signal.
del trade.orders[trade.orders.index(order)]
return False
def validate_row(
self, data: Dict, pair: str, row_index: int, current_time: datetime) -> Optional[Tuple]:
try:
# Row is treated as "current incomplete candle".
# Buy / sell signals are shifted by 1 to compensate for this.
row = data[pair][row_index]
except IndexError:
# missing Data for one pair at the end.
# Warnings for this are shown during data loading
return None
# Waits until the time-counter reaches the start of the data for this pair.
if row[DATE_IDX] > current_time:
return None
return row
def backtest(self, processed: Dict, def backtest(self, processed: Dict,
start_date: datetime, end_date: datetime, start_date: datetime, end_date: datetime,
max_open_trades: int = 0, position_stacking: bool = False, max_open_trades: int = 0, position_stacking: bool = False,
@ -605,14 +725,15 @@ class Backtesting:
""" """
trades: List[LocalTrade] = [] trades: List[LocalTrade] = []
self.prepare_backtest(enable_protections) self.prepare_backtest(enable_protections)
# Ensure wallets are uptodate (important for --strategy-list)
self.wallets.update()
# Use dict of lists with data for performance # Use dict of lists with data for performance
# (looping lists is a lot faster than pandas DataFrames) # (looping lists is a lot faster than pandas DataFrames)
data: Dict = self._get_ohlcv_as_lists(processed) data: Dict = self._get_ohlcv_as_lists(processed)
# Indexes per pair, so some pairs are allowed to have a missing start. # Indexes per pair, so some pairs are allowed to have a missing start.
indexes: Dict = defaultdict(int) indexes: Dict = defaultdict(int)
tmp = start_date + timedelta(minutes=self.timeframe_min) current_time = start_date + timedelta(minutes=self.timeframe_min)
open_trades: Dict[str, List[LocalTrade]] = defaultdict(list) open_trades: Dict[str, List[LocalTrade]] = defaultdict(list)
open_trade_count = 0 open_trade_count = 0
@ -621,35 +742,27 @@ class Backtesting:
(end_date - start_date) / timedelta(minutes=self.timeframe_min))) (end_date - start_date) / timedelta(minutes=self.timeframe_min)))
# Loop timerange and get candle for each pair at that point in time # Loop timerange and get candle for each pair at that point in time
while tmp <= end_date: while current_time <= end_date:
open_trade_count_start = open_trade_count open_trade_count_start = open_trade_count
self.check_abort() self.check_abort()
for i, pair in enumerate(data): for i, pair in enumerate(data):
row_index = indexes[pair] row_index = indexes[pair]
try: row = self.validate_row(data, pair, row_index, current_time)
# Row is treated as "current incomplete candle". if not row:
# Buy / sell signals are shifted by 1 to compensate for this.
row = data[pair][row_index]
except IndexError:
# missing Data for one pair at the end.
# Warnings for this are shown during data loading
continue
# Waits until the time-counter reaches the start of the data for this pair.
if row[DATE_IDX] > tmp:
continue continue
row_index += 1 row_index += 1
indexes[pair] = row_index indexes[pair] = row_index
self.dataprovider._set_dataframe_max_index(row_index) self.dataprovider._set_dataframe_max_index(row_index)
# 1. Process buys.
# without positionstacking, we can only have one open trade per pair. # without positionstacking, we can only have one open trade per pair.
# max_open_trades must be respected # max_open_trades must be respected
# don't open on the last row # don't open on the last row
if ( if (
(position_stacking or len(open_trades[pair]) == 0) (position_stacking or len(open_trades[pair]) == 0)
and self.trade_slot_available(max_open_trades, open_trade_count_start) and self.trade_slot_available(max_open_trades, open_trade_count_start)
and tmp != end_date and current_time != end_date
and row[BUY_IDX] == 1 and row[BUY_IDX] == 1
and row[SELL_IDX] != 1 and row[SELL_IDX] != 1
and not PairLocks.is_pair_locked(pair, row[DATE_IDX]) and not PairLocks.is_pair_locked(pair, row[DATE_IDX])
@ -657,32 +770,51 @@ class Backtesting:
trade = self._enter_trade(pair, row) trade = self._enter_trade(pair, row)
if trade: if trade:
# TODO: hacky workaround to avoid opening > max_open_trades # TODO: hacky workaround to avoid opening > max_open_trades
# This emulates previous behaviour - not sure if this is correct # This emulates previous behavior - not sure if this is correct
# Prevents buying if the trade-slot was freed in this candle # Prevents buying if the trade-slot was freed in this candle
open_trade_count_start += 1 open_trade_count_start += 1
open_trade_count += 1 open_trade_count += 1
# logger.debug(f"{pair} - Emulate creation of new trade: {trade}.") # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.")
open_trades[pair].append(trade) open_trades[pair].append(trade)
LocalTrade.add_bt_trade(trade)
for trade in list(open_trades[pair]): for trade in list(open_trades[pair]):
# also check the buying candle for sell conditions. # 2. Process buy orders.
trade_entry = self._get_sell_trade_entry(trade, row) order = trade.select_order('buy', is_open=True)
# Sell occurred if order and self._get_order_filled(order.price, row):
if trade_entry: order.close_bt_order(current_time)
trade.open_order_id = None
LocalTrade.add_bt_trade(trade)
self.wallets.update()
# 3. Create sell orders (if any)
if not trade.open_order_id:
self._get_sell_trade_entry(trade, row) # Place sell order if necessary
# 4. Process sell orders.
order = trade.select_order('sell', is_open=True)
if order and self._get_order_filled(order.price, row):
trade.open_order_id = None
trade.close_date = current_time
trade.close(order.price, show_msg=False)
# logger.debug(f"{pair} - Backtesting sell {trade}") # logger.debug(f"{pair} - Backtesting sell {trade}")
open_trade_count -= 1 open_trade_count -= 1
open_trades[pair].remove(trade) open_trades[pair].remove(trade)
LocalTrade.close_bt_trade(trade) LocalTrade.close_bt_trade(trade)
trades.append(trade_entry) trades.append(trade)
if enable_protections: self.wallets.update()
self.protections.stop_per_pair(pair, row[DATE_IDX]) self.run_protections(enable_protections, pair, current_time)
self.protections.global_stop(tmp)
# 5. Cancel expired buy/sell orders.
if self.check_order_cancel(trade, current_time):
# Close trade due to buy timeout expiration.
open_trade_count -= 1
open_trades[pair].remove(trade)
self.wallets.update()
# Move time one configured time_interval ahead. # Move time one configured time_interval ahead.
self.progress.increment() self.progress.increment()
tmp += timedelta(minutes=self.timeframe_min) current_time += timedelta(minutes=self.timeframe_min)
trades += self.handle_left_open(open_trades, data=data) trades += self.handle_left_open(open_trades, data=data)
self.wallets.update() self.wallets.update()
@ -693,6 +825,8 @@ class Backtesting:
'config': self.strategy.config, 'config': self.strategy.config,
'locks': PairLocks.get_all_locks(), 'locks': PairLocks.get_all_locks(),
'rejected_signals': self.rejected_trades, 'rejected_signals': self.rejected_trades,
'timedout_entry_orders': self.timedout_entry_orders,
'timedout_exit_orders': self.timedout_exit_orders,
'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']),
} }

View File

@ -0,0 +1,30 @@
"""
ProfitDrawDownHyperOptLoss
This module defines the alternative HyperOptLoss class based on Profit &
Drawdown objective which can be used for Hyperoptimization.
Possible to change `DRAWDOWN_MULT` to penalize drawdown objective for
individual needs.
"""
from pandas import DataFrame
from freqtrade.data.btanalysis import calculate_max_drawdown
from freqtrade.optimize.hyperopt import IHyperOptLoss
# higher numbers penalize drawdowns more severely
DRAWDOWN_MULT = 0.075
class ProfitDrawDownHyperOptLoss(IHyperOptLoss):
@staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int, *args, **kwargs) -> float:
total_profit = results["profit_abs"].sum()
try:
max_drawdown_abs = calculate_max_drawdown(results, value_col="profit_abs")[5]
except ValueError:
max_drawdown_abs = 0
return -1 * (total_profit * (1 - max_drawdown_abs * DRAWDOWN_MULT))

View File

@ -373,7 +373,7 @@ class HyperoptTools():
trials[f"Max Drawdown{' (Acct)' if has_account_drawdown else ''}"] = trials.apply( trials[f"Max Drawdown{' (Acct)' if has_account_drawdown else ''}"] = trials.apply(
lambda x: "{} {}".format( lambda x: "{} {}".format(
round_coin_value(x['max_drawdown_abs'], stake_currency), round_coin_value(x['max_drawdown_abs'], stake_currency, keep_trailing_zeros=True),
(f"({x['max_drawdown_account']:,.2%})" (f"({x['max_drawdown_account']:,.2%})"
if has_account_drawdown if has_account_drawdown
else f"({x['max_drawdown']:,.2%})" else f"({x['max_drawdown']:,.2%})"
@ -388,7 +388,7 @@ class HyperoptTools():
trials['Profit'] = trials.apply( trials['Profit'] = trials.apply(
lambda x: '{} {}'.format( lambda x: '{} {}'.format(
round_coin_value(x['Total profit'], stake_currency), round_coin_value(x['Total profit'], stake_currency, keep_trailing_zeros=True),
f"({x['Profit']:,.2%})".rjust(10, ' ') f"({x['Profit']:,.2%})".rjust(10, ' ')
).rjust(25+len(stake_currency)) ).rjust(25+len(stake_currency))
if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)), if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)),

View File

@ -436,6 +436,8 @@ def generate_strategy_stats(pairlist: List[str],
'dry_run_wallet': starting_balance, 'dry_run_wallet': starting_balance,
'final_balance': content['final_balance'], 'final_balance': content['final_balance'],
'rejected_signals': content['rejected_signals'], 'rejected_signals': content['rejected_signals'],
'timedout_entry_orders': content['timedout_entry_orders'],
'timedout_exit_orders': content['timedout_exit_orders'],
'max_open_trades': max_open_trades, 'max_open_trades': max_open_trades,
'max_open_trades_setting': (config['max_open_trades'] 'max_open_trades_setting': (config['max_open_trades']
if config['max_open_trades'] != float('inf') else -1), if config['max_open_trades'] != float('inf') else -1),
@ -726,6 +728,9 @@ def text_table_add_metrics(strat_results: Dict) -> str:
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')), ('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')),
('Entry/Exit Timeouts',
f"{strat_results.get('timedout_entry_orders', 'N/A')} / "
f"{strat_results.get('timedout_exit_orders', 'N/A')}"),
('', ''), # Empty line to improve readability ('', ''), # Empty line to improve readability
('Min balance', round_coin_value(strat_results['csum_min'], ('Min balance', round_coin_value(strat_results['csum_min'],

View File

@ -28,7 +28,36 @@ def get_backup_name(tabs, backup_prefix: str):
return table_back_name return table_back_name
def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, cols: List): def get_last_sequence_ids(engine, trade_back_name, order_back_name):
order_id: int = None
trade_id: int = None
if engine.name == 'postgresql':
with engine.begin() as connection:
trade_id = connection.execute(text("select nextval('trades_id_seq')")).fetchone()[0]
order_id = connection.execute(text("select nextval('orders_id_seq')")).fetchone()[0]
with engine.begin() as connection:
connection.execute(text(
f"ALTER SEQUENCE orders_id_seq rename to {order_back_name}_id_seq_bak"))
connection.execute(text(
f"ALTER SEQUENCE trades_id_seq rename to {trade_back_name}_id_seq_bak"))
return order_id, trade_id
def set_sequence_ids(engine, order_id, trade_id):
if engine.name == 'postgresql':
with engine.begin() as connection:
if order_id:
connection.execute(text(f"ALTER SEQUENCE orders_id_seq RESTART WITH {order_id}"))
if trade_id:
connection.execute(text(f"ALTER SEQUENCE trades_id_seq RESTART WITH {trade_id}"))
def migrate_trades_and_orders_table(
decl_base, inspector, engine,
trade_back_name: str, cols: List,
order_back_name: str, cols_order: List):
fee_open = get_column_def(cols, 'fee_open', 'fee') fee_open = get_column_def(cols, 'fee_open', 'fee')
fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null') fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null')
fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null') fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null')
@ -64,11 +93,20 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
# Schema migration necessary # Schema migration necessary
with engine.begin() as connection: with engine.begin() as connection:
connection.execute(text(f"alter table trades rename to {table_back_name}")) connection.execute(text(f"alter table trades rename to {trade_back_name}"))
with engine.begin() as connection: with engine.begin() as connection:
# drop indexes on backup table in new session # drop indexes on backup table in new session
for index in inspector.get_indexes(table_back_name): for index in inspector.get_indexes(trade_back_name):
if engine.name == 'mysql':
connection.execute(text(f"drop index {index['name']} on {trade_back_name}"))
else:
connection.execute(text(f"drop index {index['name']}")) connection.execute(text(f"drop index {index['name']}"))
order_id, trade_id = get_last_sequence_ids(engine, trade_back_name, order_back_name)
drop_orders_table(engine, order_back_name)
# let SQLAlchemy create the schema as required # let SQLAlchemy create the schema as required
decl_base.metadata.create_all(engine) decl_base.metadata.create_all(engine)
@ -100,9 +138,12 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
{sell_order_status} sell_order_status, {sell_order_status} sell_order_status,
{strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe, {strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe,
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs
from {table_back_name} from {trade_back_name}
""")) """))
migrate_orders_table(engine, order_back_name, cols_order)
set_sequence_ids(engine, order_id, trade_id)
def migrate_open_orders_to_trades(engine): def migrate_open_orders_to_trades(engine):
with engine.begin() as connection: with engine.begin() as connection:
@ -121,31 +162,39 @@ def migrate_open_orders_to_trades(engine):
""")) """))
def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, cols: List): def drop_orders_table(engine, table_back_name: str):
# Schema migration necessary # Drop and recreate orders table as backup
# This drops foreign keys, too.
with engine.begin() as connection: with engine.begin() as connection:
connection.execute(text(f"alter table orders rename to {table_back_name}")) connection.execute(text(f"create table {table_back_name} as select * from orders"))
connection.execute(text("drop table orders"))
with engine.begin() as connection:
# drop indexes on backup table in new session def migrate_orders_table(engine, table_back_name: str, cols_order: List):
for index in inspector.get_indexes(table_back_name):
connection.execute(text(f"drop index {index['name']}")) ft_fee_base = get_column_def(cols_order, 'ft_fee_base', 'null')
# let SQLAlchemy create the schema as required # let SQLAlchemy create the schema as required
decl_base.metadata.create_all(engine)
with engine.begin() as connection: with engine.begin() as connection:
connection.execute(text(f""" connection.execute(text(f"""
insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
status, symbol, order_type, side, price, amount, filled, average, remaining, cost, status, symbol, order_type, side, price, amount, filled, average, remaining, cost,
order_date, order_filled_date, order_update_date) order_date, order_filled_date, order_update_date, ft_fee_base)
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
status, symbol, order_type, side, price, amount, filled, null average, remaining, cost, status, symbol, order_type, side, price, amount, filled, null average, remaining, cost,
order_date, order_filled_date, order_update_date order_date, order_filled_date, order_update_date, {ft_fee_base}
from {table_back_name} from {table_back_name}
""")) """))
def set_sqlite_to_wal(engine):
if engine.name == 'sqlite' and str(engine.url) != 'sqlite://':
# Set Mode to
with engine.begin() as connection:
connection.execute(text("PRAGMA journal_mode=wal"))
def check_migrate(engine, decl_base, previous_tables) -> None: def check_migrate(engine, decl_base, previous_tables) -> None:
""" """
Checks if migration is necessary and migrates if necessary Checks if migration is necessary and migrates if necessary
@ -153,26 +202,22 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
inspector = inspect(engine) inspector = inspect(engine)
cols = inspector.get_columns('trades') cols = inspector.get_columns('trades')
cols_orders = inspector.get_columns('orders')
tabs = get_table_names_for_table(inspector, 'trades') tabs = get_table_names_for_table(inspector, 'trades')
table_back_name = get_backup_name(tabs, 'trades_bak') table_back_name = get_backup_name(tabs, 'trades_bak')
order_tabs = get_table_names_for_table(inspector, 'orders')
order_table_bak_name = get_backup_name(order_tabs, 'orders_bak')
# Check for latest column # Check if migration necessary
if not has_column(cols, 'buy_tag'): # Migrates both trades and orders table!
logger.info(f'Running database migration for trades - backup: {table_back_name}') # if not has_column(cols, 'buy_tag'):
migrate_trades_table(decl_base, inspector, engine, table_back_name, cols) if 'orders' not in previous_tables or not has_column(cols_orders, 'ft_fee_base'):
# Reread columns - the above recreated the table! logger.info(f"Running database migration for trades - "
inspector = inspect(engine) f"backup: {table_back_name}, {order_table_bak_name}")
cols = inspector.get_columns('trades') migrate_trades_and_orders_table(
decl_base, inspector, engine, table_back_name, cols, order_table_bak_name, cols_orders)
if 'orders' not in previous_tables and 'trades' in previous_tables: if 'orders' not in previous_tables and 'trades' in previous_tables:
logger.info('Moving open orders to Orders table.') logger.info('Moving open orders to Orders table.')
migrate_open_orders_to_trades(engine) migrate_open_orders_to_trades(engine)
else: set_sqlite_to_wal(engine)
cols_order = inspector.get_columns('orders')
if not has_column(cols_order, 'average'):
tabs = get_table_names_for_table(inspector, 'orders')
# Empty for now - as there is only one iteration of the orders table so far.
table_back_name = get_backup_name(tabs, 'orders_bak')
migrate_orders_table(decl_base, inspector, engine, table_back_name, cols)

View File

@ -16,7 +16,6 @@ from sqlalchemy.sql.schema import UniqueConstraint
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES
from freqtrade.enums import SellType from freqtrade.enums import SellType
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.misc import safe_value_fallback
from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.migrations import check_migrate
@ -39,6 +38,9 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None:
""" """
kwargs = {} kwargs = {}
if db_url == 'sqlite:///':
raise OperationalException(
f'Bad db-url {db_url}. For in-memory database, please use `sqlite://`.')
if db_url == 'sqlite://': if db_url == 'sqlite://':
kwargs.update({ kwargs.update({
'poolclass': StaticPool, 'poolclass': StaticPool,
@ -113,14 +115,15 @@ class Order(_DECL_BASE):
trade = relationship("Trade", back_populates="orders") trade = relationship("Trade", back_populates="orders")
ft_order_side = Column(String(25), nullable=False) # order_side can only be 'buy', 'sell' or 'stoploss'
ft_pair = Column(String(25), nullable=False) ft_order_side: str = Column(String(25), nullable=False)
ft_pair: str = Column(String(25), nullable=False)
ft_is_open = Column(Boolean, nullable=False, default=True, index=True) ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
order_id = Column(String(255), nullable=False, index=True) order_id = Column(String(255), nullable=False, index=True)
status = Column(String(255), nullable=True) status = Column(String(255), nullable=True)
symbol = Column(String(25), nullable=True) symbol = Column(String(25), nullable=True)
order_type = Column(String(50), nullable=True) order_type: str = Column(String(50), nullable=True)
side = Column(String(25), nullable=True) side = Column(String(25), nullable=True)
price = Column(Float, nullable=True) price = Column(Float, nullable=True)
average = Column(Float, nullable=True) average = Column(Float, nullable=True)
@ -132,6 +135,29 @@ class Order(_DECL_BASE):
order_filled_date = Column(DateTime, nullable=True) order_filled_date = Column(DateTime, nullable=True)
order_update_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True)
ft_fee_base = Column(Float, nullable=True)
@property
def order_date_utc(self) -> datetime:
""" Order-date with UTC timezoneinfo"""
return self.order_date.replace(tzinfo=timezone.utc)
@property
def safe_price(self) -> float:
return self.average or self.price
@property
def safe_filled(self) -> float:
return self.filled or self.amount or 0.0
@property
def safe_fee_base(self) -> float:
return self.ft_fee_base or 0.0
@property
def safe_amount_after_fee(self) -> float:
return self.safe_filled - self.safe_fee_base
def __repr__(self): def __repr__(self):
return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, '
@ -165,6 +191,35 @@ class Order(_DECL_BASE):
self.order_filled_date = datetime.now(timezone.utc) self.order_filled_date = datetime.now(timezone.utc)
self.order_update_date = datetime.now(timezone.utc) self.order_update_date = datetime.now(timezone.utc)
def to_json(self) -> Dict[str, Any]:
return {
'amount': self.amount,
'average': round(self.average, 8) if self.average else 0,
'cost': self.cost if self.cost else 0,
'filled': self.filled,
'ft_order_side': self.ft_order_side,
'is_open': self.ft_is_open,
'order_date': self.order_date.strftime(DATETIME_PRINT_FORMAT)
if self.order_date else None,
'order_timestamp': int(self.order_date.replace(
tzinfo=timezone.utc).timestamp() * 1000) if self.order_date else None,
'order_filled_date': self.order_filled_date.strftime(DATETIME_PRINT_FORMAT)
if self.order_filled_date else None,
'order_filled_timestamp': int(self.order_filled_date.replace(
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
'order_type': self.order_type,
'pair': self.ft_pair,
'price': self.price,
'remaining': self.remaining,
'status': self.status,
}
def close_bt_order(self, close_date: datetime):
self.order_filled_date = close_date
self.filled = self.amount
self.status = 'closed'
self.ft_is_open = False
@staticmethod @staticmethod
def update_orders(orders: List['Order'], order: Dict[str, Any]): def update_orders(orders: List['Order'], order: Dict[str, Any]):
""" """
@ -282,6 +337,16 @@ class LocalTrade():
return self.close_date.replace(tzinfo=timezone.utc) return self.close_date.replace(tzinfo=timezone.utc)
def to_json(self) -> Dict[str, Any]: def to_json(self) -> Dict[str, Any]:
filled_orders = self.select_filled_orders()
filled_entries = []
filled_exits = []
if len(filled_orders) > 0:
for order in filled_orders:
if order.ft_order_side == 'buy':
filled_entries.append(order.to_json())
if order.ft_order_side == 'sell':
filled_exits.append(order.to_json())
return { return {
'trade_id': self.id, 'trade_id': self.id,
'pair': self.pair, 'pair': self.pair,
@ -345,6 +410,8 @@ class LocalTrade():
'max_rate': self.max_rate, 'max_rate': self.max_rate,
'open_order_id': self.open_order_id, 'open_order_id': self.open_order_id,
'filled_entry_orders': filled_entries,
'filled_exit_orders': filled_exits,
} }
@staticmethod @staticmethod
@ -407,40 +474,39 @@ class LocalTrade():
f"Trailing stoploss saved us: " f"Trailing stoploss saved us: "
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.") f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.")
def update(self, order: Dict) -> None: def update_trade(self, order: Order) -> None:
""" """
Updates this entity with amount and actual open/close rates. Updates this entity with amount and actual open/close rates.
:param order: order retrieved by exchange.fetch_order() :param order: order retrieved by exchange.fetch_order()
:return: None :return: None
""" """
order_type = order['type']
# Ignore open and cancelled orders # Ignore open and cancelled orders
if order['status'] == 'open' or safe_value_fallback(order, 'average', 'price') is None: if order.status == 'open' or order.safe_price is None:
return return
logger.info('Updating trade (id=%s) ...', self.id) logger.info(f'Updating trade (id={self.id}) ...')
if order_type in ('market', 'limit') and order['side'] == 'buy': if order.ft_order_side == 'buy':
# Update open rate and actual amount # Update open rate and actual amount
self.open_rate = float(safe_value_fallback(order, 'average', 'price')) self.open_rate = order.safe_price
self.amount = float(safe_value_fallback(order, 'filled', 'amount')) self.amount = order.safe_amount_after_fee
if self.is_open: if self.is_open:
logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') logger.info(f'{order.order_type.upper()}_BUY has been fulfilled for {self}.')
self.open_order_id = None self.open_order_id = None
self.recalc_trade_from_orders() self.recalc_trade_from_orders()
elif order_type in ('market', 'limit') and order['side'] == 'sell': elif order.ft_order_side == 'sell':
if self.is_open: if self.is_open:
logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') logger.info(f'{order.order_type.upper()}_SELL has been fulfilled for {self}.')
self.close(safe_value_fallback(order, 'average', 'price')) self.close(order.safe_price)
elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): elif order.ft_order_side == 'stoploss':
self.stoploss_order_id = None self.stoploss_order_id = None
self.close_rate_requested = self.stop_loss self.close_rate_requested = self.stop_loss
self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
if self.is_open: if self.is_open:
logger.info(f'{order_type.upper()} is hit for {self}.') logger.info(f'{order.order_type.upper()} is hit for {self}.')
self.close(safe_value_fallback(order, 'average', 'price')) self.close(order.safe_price)
else: else:
raise ValueError(f'Unknown order type: {order_type}') raise ValueError(f'Unknown order type: {order.order_type}')
Trade.commit() Trade.commit()
def close(self, rate: float, *, show_msg: bool = True) -> None: def close(self, rate: float, *, show_msg: bool = True) -> None:
@ -583,7 +649,7 @@ class LocalTrade():
(o.status not in NON_OPEN_EXCHANGE_STATES)): (o.status not in NON_OPEN_EXCHANGE_STATES)):
continue continue
tmp_amount = o.amount tmp_amount = o.safe_amount_after_fee
tmp_price = o.average or o.price tmp_price = o.average or o.price
if o.filled is not None: if o.filled is not None:
tmp_amount = o.filled tmp_amount = o.filled
@ -600,14 +666,27 @@ class LocalTrade():
if self.stop_loss_pct is not None and self.open_rate is not None: if self.stop_loss_pct is not None and self.open_rate is not None:
self.adjust_stop_loss(self.open_rate, self.stop_loss_pct) self.adjust_stop_loss(self.open_rate, self.stop_loss_pct)
def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: def select_order_by_order_id(self, order_id: str) -> Optional[Order]:
"""
Finds order object by Order id.
:param order_id: Exchange order id
"""
for o in self.orders:
if o.order_id == order_id:
return o
return None
def select_order(
self, order_side: str = None, is_open: Optional[bool] = None) -> Optional[Order]:
""" """
Finds latest order for this orderside and status Finds latest order for this orderside and status
:param order_side: Side of the order (either 'buy' or 'sell') :param order_side: ft_order_side of the order (either 'buy', 'sell' or 'stoploss')
:param is_open: Only search for open orders? :param is_open: Only search for open orders?
:return: latest Order object if it exists, else None :return: latest Order object if it exists, else None
""" """
orders = [o for o in self.orders if o.side == order_side] orders = self.orders
if order_side:
orders = [o for o in self.orders if o.ft_order_side == order_side]
if is_open is not None: if is_open is not None:
orders = [o for o in orders if o.ft_is_open == is_open] orders = [o for o in orders if o.ft_is_open == is_open]
if len(orders) > 0: if len(orders) > 0:
@ -615,14 +694,14 @@ class LocalTrade():
else: else:
return None return None
def select_filled_orders(self, order_side: str) -> List['Order']: def select_filled_orders(self, order_side: Optional[str] = None) -> List['Order']:
""" """
Finds filled orders for this orderside. Finds filled orders for this orderside.
:param order_side: Side of the order (either 'buy' or 'sell') :param order_side: Side of the order (either 'buy', 'sell', or None)
:return: array of Order objects :return: array of Order objects
""" """
return [o for o in self.orders if o.ft_order_side == order_side and return [o for o in self.orders if ((o.ft_order_side == order_side) or (order_side is None))
o.ft_is_open is False and and o.ft_is_open is False and
(o.filled or 0) > 0 and (o.filled or 0) > 0 and
o.status in NON_OPEN_EXCHANGE_STATES] o.status in NON_OPEN_EXCHANGE_STATES]
@ -741,11 +820,11 @@ class Trade(_DECL_BASE, LocalTrade):
fee_close = Column(Float, nullable=False, default=0.0) fee_close = Column(Float, nullable=False, default=0.0)
fee_close_cost = Column(Float, nullable=True) fee_close_cost = Column(Float, nullable=True)
fee_close_currency = Column(String(25), nullable=True) fee_close_currency = Column(String(25), nullable=True)
open_rate = Column(Float) open_rate: float = Column(Float)
open_rate_requested = Column(Float) open_rate_requested = Column(Float)
# open_trade_value - calculated via _calc_open_trade_value # open_trade_value - calculated via _calc_open_trade_value
open_trade_value = Column(Float) open_trade_value = Column(Float)
close_rate = Column(Float) close_rate: Optional[float] = Column(Float)
close_rate_requested = Column(Float) close_rate_requested = Column(Float)
close_profit = Column(Float) close_profit = Column(Float)
close_profit_abs = Column(Float) close_profit_abs = Column(Float)

View File

@ -61,8 +61,8 @@ def init_plotscript(config, markets: List, startup_candles: int = 0):
startup_candles, min_date) startup_candles, min_date)
no_trades = False no_trades = False
filename = config.get('exportfilename') filename = config.get("exportfilename")
if config.get('no_trades', False): if config.get("no_trades", False):
no_trades = True no_trades = True
elif config['trade_source'] == 'file': elif config['trade_source'] == 'file':
if not filename.is_dir() and not filename.is_file(): if not filename.is_dir() and not filename.is_file():

View File

@ -60,6 +60,7 @@ class PerformanceFilter(IPairList):
# Get pairlist from performance dataframe values # Get pairlist from performance dataframe values
list_df = pd.DataFrame({'pair': pairlist}) list_df = pd.DataFrame({'pair': pairlist})
list_df['prior_idx'] = list_df.index
# Set initial value for pairs with no trades to 0 # Set initial value for pairs with no trades to 0
# Sort the list using: # Sort the list using:
@ -67,7 +68,7 @@ class PerformanceFilter(IPairList):
# - then count (low to high, so as to favor same performance with fewer trades) # - then count (low to high, so as to favor same performance with fewer trades)
# - then pair name alphametically # - then pair name alphametically
sorted_df = list_df.merge(performance, on='pair', how='left')\ sorted_df = list_df.merge(performance, on='pair', how='left')\
.fillna(0).sort_values(by=['count', 'pair'], ascending=True)\ .fillna(0).sort_values(by=['count', 'prior_idx'], ascending=True)\
.sort_values(by=['profit_ratio'], ascending=False) .sort_values(by=['profit_ratio'], ascending=False)
if self._min_profit is not None: if self._min_profit is not None:
removed = sorted_df[sorted_df['profit_ratio'] < self._min_profit] removed = sorted_df[sorted_df['profit_ratio'] < self._min_profit]

View File

@ -8,7 +8,7 @@ from freqtrade.configuration.config_validation import validate_config_consistenc
from freqtrade.enums import BacktestState from freqtrade.enums import BacktestState
from freqtrade.exceptions import DependencyException from freqtrade.exceptions import DependencyException
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse
from freqtrade.rpc.api_server.deps import get_config from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode
from freqtrade.rpc.api_server.webserver import ApiServer from freqtrade.rpc.api_server.webserver import ApiServer
from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.rpc import RPCException
@ -20,8 +20,9 @@ router = APIRouter()
@router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) @router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
# flake8: noqa: C901
async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks, async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks,
config=Depends(get_config)): config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
"""Start backtesting if not done so already""" """Start backtesting if not done so already"""
if ApiServer._bgtask_running: if ApiServer._bgtask_running:
raise RPCException('Bot Background task already running') raise RPCException('Bot Background task already running')
@ -32,6 +33,10 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
for setting in settings.keys(): for setting in settings.keys():
if settings[setting] is not None: if settings[setting] is not None:
btconfig[setting] = settings[setting] btconfig[setting] = settings[setting]
try:
btconfig['stake_amount'] = float(btconfig['stake_amount'])
except ValueError:
pass
# Force dry-run for backtesting # Force dry-run for backtesting
btconfig['dry_run'] = True btconfig['dry_run'] = True
@ -57,7 +62,6 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
): ):
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
ApiServer._bt = Backtesting(btconfig) ApiServer._bt = Backtesting(btconfig)
if ApiServer._bt.timeframe_detail:
ApiServer._bt.load_bt_data_detail() ApiServer._bt.load_bt_data_detail()
else: else:
ApiServer._bt.config = btconfig ApiServer._bt.config = btconfig
@ -117,7 +121,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
@router.get('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) @router.get('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_get_backtest(): def api_get_backtest(ws_mode=Depends(is_webserver_mode)):
""" """
Get backtesting result. Get backtesting result.
Returns Result after backtesting has been ran. Returns Result after backtesting has been ran.
@ -153,7 +157,7 @@ def api_get_backtest():
@router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) @router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_delete_backtest(): def api_delete_backtest(ws_mode=Depends(is_webserver_mode)):
"""Reset backtesting""" """Reset backtesting"""
if ApiServer._bgtask_running: if ApiServer._bgtask_running:
return { return {
@ -179,7 +183,7 @@ def api_delete_backtest():
@router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest']) @router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_backtest_abort(): def api_backtest_abort(ws_mode=Depends(is_webserver_mode)):
if not ApiServer._bgtask_running: if not ApiServer._bgtask_running:
return { return {
"status": "not_running", "status": "not_running",

View File

@ -109,7 +109,7 @@ class SellReason(BaseModel):
class Stats(BaseModel): class Stats(BaseModel):
sell_reasons: Dict[str, SellReason] sell_reasons: Dict[str, SellReason]
durations: Dict[str, Union[str, float]] durations: Dict[str, Optional[float]]
class DailyRecord(BaseModel): class DailyRecord(BaseModel):
@ -149,7 +149,7 @@ class ShowConfig(BaseModel):
api_version: float api_version: float
dry_run: bool dry_run: bool
stake_currency: str stake_currency: str
stake_amount: Union[float, str] stake_amount: str
available_capital: Optional[float] available_capital: Optional[float]
stake_currency_decimals: int stake_currency_decimals: int
max_open_trades: int max_open_trades: int
@ -280,6 +280,7 @@ class ForceBuyPayload(BaseModel):
price: Optional[float] price: Optional[float]
ordertype: Optional[OrderTypeValues] ordertype: Optional[OrderTypeValues]
stakeamount: Optional[float] stakeamount: Optional[float]
entry_tag: Optional[str]
class ForceSellPayload(BaseModel): class ForceSellPayload(BaseModel):
@ -365,7 +366,7 @@ class BacktestRequest(BaseModel):
timeframe_detail: Optional[str] timeframe_detail: Optional[str]
timerange: Optional[str] timerange: Optional[str]
max_open_trades: Optional[int] max_open_trades: Optional[int]
stake_amount: Optional[Union[float, str]] stake_amount: Optional[str]
enable_protections: bool enable_protections: bool
dry_run_wallet: Optional[float] dry_run_wallet: Optional[float]
@ -384,3 +385,8 @@ class BacktestResponse(BaseModel):
class SysInfo(BaseModel): class SysInfo(BaseModel):
cpu_pct: List[float] cpu_pct: List[float]
ram_pct: float ram_pct: float
class Health(BaseModel):
last_process: datetime
last_process_ts: int

View File

@ -14,12 +14,12 @@ from freqtrade.rpc import RPC
from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload,
BlacklistResponse, Count, Daily, BlacklistResponse, Count, Daily,
DeleteLockRequest, DeleteTrade, ForceBuyPayload, DeleteLockRequest, DeleteTrade, ForceBuyPayload,
ForceBuyResponse, ForceSellPayload, Locks, Logs, ForceBuyResponse, ForceSellPayload, Health, Locks,
OpenTradeSchema, PairHistory, PerformanceEntry, Logs, OpenTradeSchema, PairHistory,
Ping, PlotConfig, Profit, ResultMsg, ShowConfig, PerformanceEntry, Ping, PlotConfig, Profit,
Stats, StatusMsg, StrategyListResponse, ResultMsg, ShowConfig, Stats, StatusMsg,
StrategyResponse, SysInfo, Version, StrategyListResponse, StrategyResponse, SysInfo,
WhitelistResponse) Version, WhitelistResponse)
from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional
from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.rpc import RPCException
@ -136,8 +136,9 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None ordertype = payload.ordertype.value if payload.ordertype else None
stake_amount = payload.stakeamount if payload.stakeamount else None stake_amount = payload.stakeamount if payload.stakeamount else None
entry_tag = payload.entry_tag if payload.entry_tag else None
trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, stake_amount) trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, stake_amount, entry_tag)
if trade: if trade:
return ForceBuyResponse.parse_obj(trade.to_json()) return ForceBuyResponse.parse_obj(trade.to_json())
@ -291,3 +292,8 @@ def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Option
@router.get('/sysinfo', response_model=SysInfo, tags=['info']) @router.get('/sysinfo', response_model=SysInfo, tags=['info'])
def sysinfo(): def sysinfo():
return RPC._rpc_sysinfo() return RPC._rpc_sysinfo()
@router.get('/health', response_model=Health, tags=['info'])
def health(rpc: RPC = Depends(get_rpc)):
return rpc._health()

View File

@ -2,6 +2,7 @@ from typing import Any, Dict, Iterator, Optional
from fastapi import Depends from fastapi import Depends
from freqtrade.enums import RunMode
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.rpc import RPC, RPCException from freqtrade.rpc.rpc import RPC, RPCException
@ -38,3 +39,9 @@ def get_exchange(config=Depends(get_config)):
ApiServer._exchange = ExchangeResolver.load_exchange( ApiServer._exchange = ExchangeResolver.load_exchange(
config['exchange']['name'], config) config['exchange']['name'], config)
return ApiServer._exchange return ApiServer._exchange
def is_webserver_mode(config=Depends(get_config)):
if config['runmode'] != RunMode.WEBSERVER:
raise RPCException('Bot is not in the correct state')
return None

View File

@ -17,6 +17,15 @@ from freqtrade.constants import SUPPORTED_FIAT
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Manually map symbol to ID for some common coins
# with duplicate coingecko entries
coingecko_mapping = {
'eth': 'ethereum',
'bnb': 'binancecoin',
'sol': 'solana',
}
class CryptoToFiatConverter: class CryptoToFiatConverter:
""" """
Main class to initiate Crypto to FIAT. Main class to initiate Crypto to FIAT.
@ -77,8 +86,9 @@ class CryptoToFiatConverter:
else: else:
return None return None
found = [x for x in self._coinlistings if x['symbol'] == crypto_symbol] found = [x for x in self._coinlistings if x['symbol'] == crypto_symbol]
if crypto_symbol == 'eth':
found = [x for x in self._coinlistings if x['id'] == 'ethereum'] if crypto_symbol in coingecko_mapping.keys():
found = [x for x in self._coinlistings if x['id'] == coingecko_mapping[crypto_symbol]]
if len(found) == 1: if len(found) == 1:
return found[0]['id'] return found[0]['id']

View File

@ -10,8 +10,9 @@ from typing import Any, Dict, List, Optional, Tuple, Union
import arrow import arrow
import psutil import psutil
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from dateutil.tz import tzlocal
from numpy import NAN, inf, int64, mean from numpy import NAN, inf, int64, mean
from pandas import DataFrame from pandas import DataFrame, NaT
from freqtrade import __version__ from freqtrade import __version__
from freqtrade.configuration.timerange import TimeRange from freqtrade.configuration.timerange import TimeRange
@ -111,7 +112,7 @@ class RPC:
'dry_run': config['dry_run'], 'dry_run': config['dry_run'],
'stake_currency': config['stake_currency'], 'stake_currency': config['stake_currency'],
'stake_currency_decimals': decimals_per_coin(config['stake_currency']), 'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
'stake_amount': config['stake_amount'], 'stake_amount': str(config['stake_amount']),
'available_capital': config.get('available_capital'), 'available_capital': config.get('available_capital'),
'max_open_trades': (config['max_open_trades'] 'max_open_trades': (config['max_open_trades']
if config['max_open_trades'] != float('inf') else -1), if config['max_open_trades'] != float('inf') else -1),
@ -263,7 +264,7 @@ class RPC:
profitcol += " (" + fiat_display_currency + ")" profitcol += " (" + fiat_display_currency + ")"
if self._config.get('position_adjustment_enable', False): if self._config.get('position_adjustment_enable', False):
columns = ['ID', 'Pair', 'Since', profitcol, '# Buys'] columns = ['ID', 'Pair', 'Since', profitcol, '# Entries']
else: else:
columns = ['ID', 'Pair', 'Since', profitcol] columns = ['ID', 'Pair', 'Since', profitcol]
return trades_list, columns, fiat_profit_sum return trades_list, columns, fiat_profit_sum
@ -439,9 +440,9 @@ class RPC:
trade_dur = (trade.close_date - trade.open_date).total_seconds() trade_dur = (trade.close_date - trade.open_date).total_seconds()
dur[trade_win_loss(trade)].append(trade_dur) dur[trade_win_loss(trade)].append(trade_dur)
wins_dur = sum(dur['wins']) / len(dur['wins']) if len(dur['wins']) > 0 else 'N/A' wins_dur = sum(dur['wins']) / len(dur['wins']) if len(dur['wins']) > 0 else None
draws_dur = sum(dur['draws']) / len(dur['draws']) if len(dur['draws']) > 0 else 'N/A' draws_dur = sum(dur['draws']) / len(dur['draws']) if len(dur['draws']) > 0 else None
losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else 'N/A' losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else None
durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur} durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur}
return {'sell_reasons': sell_reasons, 'durations': durations} return {'sell_reasons': sell_reasons, 'durations': durations}
@ -598,11 +599,6 @@ class RPC:
'est_stake': est_stake or 0, 'est_stake': est_stake or 0,
'stake': stake_currency, 'stake': stake_currency,
}) })
if total == 0.0:
if self._freqtrade.config['dry_run']:
raise RPCException('Running in Dry Run, balances are not available.')
else:
raise RPCException('All balances are zero.')
value = self._fiat_converter.convert_amount( value = self._fiat_converter.convert_amount(
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
@ -716,7 +712,8 @@ class RPC:
return {'result': f'Created sell order for trade {trade_id}.'} return {'result': f'Created sell order for trade {trade_id}.'}
def _rpc_forcebuy(self, pair: str, price: Optional[float], order_type: Optional[str] = None, def _rpc_forcebuy(self, pair: str, price: Optional[float], order_type: Optional[str] = None,
stake_amount: Optional[float] = None) -> Optional[Trade]: stake_amount: Optional[float] = None,
buy_tag: Optional[str] = None) -> Optional[Trade]:
""" """
Handler for forcebuy <asset> <price> Handler for forcebuy <asset> <price>
Buys a pair trade at the given or current price Buys a pair trade at the given or current price
@ -750,7 +747,7 @@ class RPC:
order_type = self._freqtrade.strategy.order_types.get( order_type = self._freqtrade.strategy.order_types.get(
'forcebuy', self._freqtrade.strategy.order_types['buy']) 'forcebuy', self._freqtrade.strategy.order_types['buy'])
if self._freqtrade.execute_entry(pair, stake_amount, price, if self._freqtrade.execute_entry(pair, stake_amount, price,
ordertype=order_type, trade=trade): ordertype=order_type, trade=trade, buy_tag=buy_tag):
Trade.commit() Trade.commit()
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
return trade return trade
@ -962,8 +959,16 @@ class RPC:
sell_mask = (dataframe['sell'] == 1) sell_mask = (dataframe['sell'] == 1)
sell_signals = int(sell_mask.sum()) sell_signals = int(sell_mask.sum())
dataframe.loc[sell_mask, '_sell_signal_close'] = dataframe.loc[sell_mask, 'close'] dataframe.loc[sell_mask, '_sell_signal_close'] = dataframe.loc[sell_mask, 'close']
dataframe = dataframe.replace([inf, -inf], NAN)
dataframe = dataframe.replace({NAN: None}) # band-aid until this is fixed:
# https://github.com/pandas-dev/pandas/issues/45836
datetime_types = ['datetime', 'datetime64', 'datetime64[ns, UTC]']
date_columns = dataframe.select_dtypes(include=datetime_types)
for date_column in date_columns:
# replace NaT with `None`
dataframe[date_column] = dataframe[date_column].astype(object).replace({NaT: None})
dataframe = dataframe.replace({inf: None, -inf: None, NAN: None})
res = { res = {
'pair': pair, 'pair': pair,
@ -1038,3 +1043,11 @@ class RPC:
"cpu_pct": psutil.cpu_percent(interval=1, percpu=True), "cpu_pct": psutil.cpu_percent(interval=1, percpu=True),
"ram_pct": psutil.virtual_memory().percent "ram_pct": psutil.virtual_memory().percent
} }
def _health(self) -> Dict[str, Union[str, int]]:
last_p = self._freqtrade.last_process
return {
'last_process': str(last_p),
'last_process_loc': last_p.astimezone(tzlocal()).strftime(DATETIME_PRINT_FORMAT),
'last_process_ts': int(last_p.timestamp()),
}

View File

@ -113,7 +113,7 @@ class Telegram(RPCHandler):
r'/stopbuy$', r'/reload_config$', r'/show_config$', r'/stopbuy$', r'/reload_config$', r'/show_config$',
r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$', r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$',
r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$', r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
r'/forcebuy$', r'/edge$', r'/help$', r'/version$'] r'/forcebuy$', r'/edge$', r'/health$', r'/help$', r'/version$']
# Create keys for generation # Create keys for generation
valid_keys_print = [k.replace('$', '') for k in valid_keys] valid_keys_print = [k.replace('$', '') for k in valid_keys]
@ -173,6 +173,7 @@ class Telegram(RPCHandler):
CommandHandler(['blacklist_delete', 'bl_delete'], self._blacklist_delete), CommandHandler(['blacklist_delete', 'bl_delete'], self._blacklist_delete),
CommandHandler('logs', self._logs), CommandHandler('logs', self._logs),
CommandHandler('edge', self._edge), CommandHandler('edge', self._edge),
CommandHandler('health', self._health),
CommandHandler('help', self._help), CommandHandler('help', self._help),
CommandHandler('version', self._version), CommandHandler('version', self._version),
] ]
@ -369,6 +370,48 @@ class Telegram(RPCHandler):
else: else:
return "\N{CROSS MARK}" return "\N{CROSS MARK}"
def _prepare_entry_details(self, filled_orders, base_currency, is_open):
"""
Prepare details of trade with entry adjustment enabled
"""
lines = []
for x, order in enumerate(filled_orders):
cur_entry_datetime = arrow.get(order["order_filled_date"])
cur_entry_amount = order["amount"]
cur_entry_average = order["average"]
lines.append(" ")
if x == 0:
lines.append("*Entry #{}:*".format(x+1))
lines.append("*Entry Amount:* {} ({:.8f} {})"
.format(cur_entry_amount, order["cost"], base_currency))
lines.append("*Average Entry Price:* {}".format(cur_entry_average))
else:
sumA = 0
sumB = 0
for y in range(x):
sumA += (filled_orders[y]["amount"] * filled_orders[y]["average"])
sumB += filled_orders[y]["amount"]
prev_avg_price = sumA/sumB
price_to_1st_entry = ((cur_entry_average - filled_orders[0]["average"])
/ filled_orders[0]["average"])
minus_on_entry = (cur_entry_average - prev_avg_price)/prev_avg_price
dur_entry = cur_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"])
days = dur_entry.days
hours, remainder = divmod(dur_entry.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
lines.append("*Entry #{}:* at {:.2%} avg profit".format(x+1, minus_on_entry))
if is_open:
lines.append("({})".format(cur_entry_datetime
.humanize(granularity=["day", "hour", "minute"])))
lines.append("*Entry Amount:* {} ({:.8f} {})"
.format(cur_entry_amount, order["cost"], base_currency))
lines.append("*Average Entry Price:* {} ({:.2%} from 1st entry rate)"
.format(cur_entry_average, price_to_1st_entry))
lines.append("*Order filled at:* {}".format(order["order_filled_date"]))
lines.append("({}d {}h {}m {}s from previous entry)"
.format(days, hours, minutes, seconds))
return lines
@authorized_only @authorized_only
def _status(self, update: Update, context: CallbackContext) -> None: def _status(self, update: Update, context: CallbackContext) -> None:
""" """
@ -392,21 +435,37 @@ class Telegram(RPCHandler):
trade_ids = [int(i) for i in context.args if i.isnumeric()] trade_ids = [int(i) for i in context.args if i.isnumeric()]
results = self._rpc._rpc_trade_status(trade_ids=trade_ids) results = self._rpc._rpc_trade_status(trade_ids=trade_ids)
position_adjust = self._config.get('position_adjustment_enable', False)
max_entries = self._config.get('max_entry_position_adjustment', -1)
messages = [] messages = []
for r in results: for r in results:
r['open_date_hum'] = arrow.get(r['open_date']).humanize() r['open_date_hum'] = arrow.get(r['open_date']).humanize()
r['num_entries'] = len(r['filled_entry_orders'])
r['sell_reason'] = r.get('sell_reason', "")
lines = [ lines = [
"*Trade ID:* `{trade_id}` `(since {open_date_hum})`", "*Trade ID:* `{trade_id}`" +
("` (since {open_date_hum})`" if r['is_open'] else ""),
"*Current Pair:* {pair}", "*Current Pair:* {pair}",
"*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Amount:* `{amount} ({stake_amount} {base_currency})`",
"*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "", "*Entry Tag:* `{buy_tag}`" if r['buy_tag'] else "",
"*Exit Reason:* `{sell_reason}`" if r['sell_reason'] else "",
]
if position_adjust:
max_buy_str = (f"/{max_entries + 1}" if (max_entries > 0) else "")
lines.append("*Number of Entries:* `{num_entries}`" + max_buy_str)
lines.extend([
"*Open Rate:* `{open_rate:.8f}`", "*Open Rate:* `{open_rate:.8f}`",
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "", "*Close Rate:* `{close_rate:.8f}`" if r['close_rate'] else "",
"*Current Rate:* `{current_rate:.8f}`", "*Open Date:* `{open_date}`",
"*Close Date:* `{close_date}`" if r['close_date'] else "",
"*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "",
("*Current Profit:* " if r['is_open'] else "*Close Profit: *") ("*Current Profit:* " if r['is_open'] else "*Close Profit: *")
+ "`{profit_ratio:.2%}`", + "`{profit_ratio:.2%}`",
] ])
if r['is_open']:
if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
and r['initial_stop_loss_ratio'] is not None): and r['initial_stop_loss_ratio'] is not None):
# Adding initial stoploss only if it is different from stoploss # Adding initial stoploss only if it is different from stoploss
@ -424,6 +483,10 @@ class Telegram(RPCHandler):
else: else:
lines.append("*Open Order:* `{open_order}`") lines.append("*Open Order:* `{open_order}`")
lines_detail = self._prepare_entry_details(
r['filled_entry_orders'], r['base_currency'], r['is_open'])
lines.extend((lines_detail if (len(r['filled_entry_orders']) > 1) else ""))
# Filter empty lines using list-comprehension # Filter empty lines using list-comprehension
messages.append("\n".join([line for line in lines if line]).format(**r)) messages.append("\n".join([line for line in lines if line]).format(**r))
@ -703,9 +766,9 @@ class Telegram(RPCHandler):
duration_msg = tabulate( duration_msg = tabulate(
[ [
['Wins', str(timedelta(seconds=durations['wins'])) ['Wins', str(timedelta(seconds=durations['wins']))
if durations['wins'] != 'N/A' else 'N/A'], if durations['wins'] is not None else 'N/A'],
['Losses', str(timedelta(seconds=durations['losses'])) ['Losses', str(timedelta(seconds=durations['losses']))
if durations['losses'] != 'N/A' else 'N/A'] if durations['losses'] is not None else 'N/A']
], ],
headers=['', 'Avg. Duration'] headers=['', 'Avg. Duration']
) )
@ -727,12 +790,13 @@ class Telegram(RPCHandler):
output = '' output = ''
if self._config['dry_run']: if self._config['dry_run']:
output += "*Warning:* Simulated balances in Dry Mode.\n" output += "*Warning:* Simulated balances in Dry Mode.\n"
starting_cap = round_coin_value(
output += ("Starting capital: " result['starting_capital'], self._config['stake_currency'])
f"`{result['starting_capital']}` {self._config['stake_currency']}" output += f"Starting capital: `{starting_cap}`"
) starting_cap_fiat = round_coin_value(
output += (f" `{result['starting_capital_fiat']}` " result['starting_capital_fiat'], self._config['fiat_display_currency']
f"{self._config['fiat_display_currency']}.\n" ) if result['starting_capital_fiat'] > 0 else ''
output += (f" `, {starting_cap_fiat}`.\n"
) if result['starting_capital_fiat'] > 0 else '.\n' ) if result['starting_capital_fiat'] > 0 else '.\n'
total_dust_balance = 0 total_dust_balance = 0
@ -851,6 +915,7 @@ class Telegram(RPCHandler):
self._send_msg(str(e)) self._send_msg(str(e))
def _forcebuy_action(self, pair, price=None): def _forcebuy_action(self, pair, price=None):
if pair != 'cancel':
try: try:
self._rpc._rpc_forcebuy(pair, price) self._rpc._rpc_forcebuy(pair, price)
except RPCException as e: except RPCException as e:
@ -884,10 +949,13 @@ class Telegram(RPCHandler):
self._forcebuy_action(pair, price) self._forcebuy_action(pair, price)
else: else:
whitelist = self._rpc._rpc_whitelist()['whitelist'] whitelist = self._rpc._rpc_whitelist()['whitelist']
pairs = [InlineKeyboardButton(text=pair, callback_data=pair) for pair in whitelist] pair_buttons = [
InlineKeyboardButton(text=pair, callback_data=pair) for pair in sorted(whitelist)]
buttons_aligned = self._layout_inline_keyboard(pair_buttons)
buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')])
self._send_msg(msg="Which pair?", self._send_msg(msg="Which pair?",
keyboard=self._layout_inline_keyboard(pairs)) keyboard=buttons_aligned)
@authorized_only @authorized_only
def _trades(self, update: Update, context: CallbackContext) -> None: def _trades(self, update: Update, context: CallbackContext) -> None:
@ -1282,6 +1350,7 @@ class Telegram(RPCHandler):
"*/logs [limit]:* `Show latest logs - defaults to 10` \n" "*/logs [limit]:* `Show latest logs - defaults to 10` \n"
"*/count:* `Show number of active trades compared to allowed number of trades`\n" "*/count:* `Show number of active trades compared to allowed number of trades`\n"
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n" "*/edge:* `Shows validated pairs by Edge if it is enabled` \n"
"*/health* `Show latest process timestamp - defaults to 1970-01-01 00:00:00` \n"
"_Statistics_\n" "_Statistics_\n"
"------------\n" "------------\n"
@ -1309,6 +1378,19 @@ class Telegram(RPCHandler):
self._send_msg(message, parse_mode=ParseMode.MARKDOWN) self._send_msg(message, parse_mode=ParseMode.MARKDOWN)
@authorized_only
def _health(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /health
Shows the last process timestamp
"""
try:
health = self._rpc._health()
message = f"Last process: `{health['last_process_loc']}`"
self._send_msg(message)
except RPCException as e:
self._send_msg(str(e))
@authorized_only @authorized_only
def _version(self, update: Update, context: CallbackContext) -> None: def _version(self, update: Update, context: CallbackContext) -> None:
""" """

View File

@ -18,6 +18,7 @@ from freqtrade.exceptions import OperationalException, StrategyError
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.exchange.exchange import timeframe_to_next_date
from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence import PairLocks, Trade
from freqtrade.persistence.models import LocalTrade, Order
from freqtrade.strategy.hyper import HyperStrategyMixin from freqtrade.strategy.hyper import HyperStrategyMixin
from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators, from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators,
_create_and_merge_informative_pair, _create_and_merge_informative_pair,
@ -686,7 +687,7 @@ class IStrategy(ABC, HyperStrategyMixin):
else: else:
return False return False
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, def should_sell(self, trade: Trade, rate: float, current_time: datetime, buy: bool,
sell: bool, low: float = None, high: float = None, sell: bool, low: float = None, high: float = None,
force_stoploss: float = 0) -> SellCheckTuple: force_stoploss: float = 0) -> SellCheckTuple:
""" """
@ -703,7 +704,8 @@ class IStrategy(ABC, HyperStrategyMixin):
trade.adjust_min_max_rates(high or current_rate, low or current_rate) trade.adjust_min_max_rates(high or current_rate, low or current_rate)
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
current_time=date, current_profit=current_profit, current_time=current_time,
current_profit=current_profit,
force_stoploss=force_stoploss, low=low, high=high) force_stoploss=force_stoploss, low=low, high=high)
# Set current rate to high for backtesting sell # Set current rate to high for backtesting sell
@ -713,7 +715,7 @@ class IStrategy(ABC, HyperStrategyMixin):
# if buy signal and ignore_roi is set, we don't need to evaluate min_roi. # if buy signal and ignore_roi is set, we don't need to evaluate min_roi.
roi_reached = (not (buy and self.ignore_roi_if_buy_signal) roi_reached = (not (buy and self.ignore_roi_if_buy_signal)
and self.min_roi_reached(trade=trade, current_profit=current_profit, and self.min_roi_reached(trade=trade, current_profit=current_profit,
current_time=date)) current_time=current_time))
sell_signal = SellType.NONE sell_signal = SellType.NONE
custom_reason = '' custom_reason = ''
@ -729,8 +731,8 @@ class IStrategy(ABC, HyperStrategyMixin):
sell_signal = SellType.SELL_SIGNAL sell_signal = SellType.SELL_SIGNAL
else: else:
custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)(
pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate, pair=trade.pair, trade=trade, current_time=current_time,
current_profit=current_profit) current_rate=current_rate, current_profit=current_profit)
if custom_reason: if custom_reason:
sell_signal = SellType.CUSTOM_SELL sell_signal = SellType.CUSTOM_SELL
if isinstance(custom_reason, str): if isinstance(custom_reason, str):
@ -862,23 +864,22 @@ class IStrategy(ABC, HyperStrategyMixin):
else: else:
return current_profit > roi return current_profit > roi
def ft_check_timed_out(self, side: str, trade: Trade, order: Dict, def ft_check_timed_out(self, side: str, trade: LocalTrade, order: Order,
current_time: datetime) -> bool: current_time: datetime) -> bool:
""" """
FT Internal method. FT Internal method.
Check if timeout is active, and if the order is still open and timed out Check if timeout is active, and if the order is still open and timed out
""" """
timeout = self.config.get('unfilledtimeout', {}).get(side) timeout = self.config.get('unfilledtimeout', {}).get(side)
ordertime = arrow.get(order['datetime']).datetime
if timeout is not None: if timeout is not None:
timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes') timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes')
timeout_kwargs = {timeout_unit: -timeout} timeout_kwargs = {timeout_unit: -timeout}
timeout_threshold = current_time + timedelta(**timeout_kwargs) timeout_threshold = current_time + timedelta(**timeout_kwargs)
timedout = (order['status'] == 'open' and order['side'] == side timedout = (order.status == 'open' and order.side == side
and ordertime < timeout_threshold) and order.order_date_utc < timeout_threshold)
if timedout: if timedout:
return True return True
time_method = self.check_sell_timeout if order['side'] == 'sell' else self.check_buy_timeout time_method = self.check_sell_timeout if order.side == 'sell' else self.check_buy_timeout
return strategy_safe_wrapper(time_method, return strategy_safe_wrapper(time_method,
default_retval=False)( default_retval=False)(

View File

@ -211,7 +211,7 @@ class Wallets:
return stake_amount return stake_amount
def get_trade_stake_amount(self, pair: str, edge=None) -> float: def get_trade_stake_amount(self, pair: str, edge=None, update: bool = True) -> float:
""" """
Calculate stake amount for the trade Calculate stake amount for the trade
:return: float: Stake amount :return: float: Stake amount
@ -219,6 +219,7 @@ class Wallets:
""" """
stake_amount: float stake_amount: float
# Ensure wallets are uptodate. # Ensure wallets are uptodate.
if update:
self.update() self.update()
val_tied_up = Trade.total_open_trades_stakes() val_tied_up = Trade.total_open_trades_stakes()
available_amount = self.get_available_stake_amount() available_amount = self.get_available_stake_amount()

View File

@ -7,8 +7,8 @@ coveralls==3.3.1
flake8==4.0.1 flake8==4.0.1
flake8-tidy-imports==4.6.0 flake8-tidy-imports==4.6.0
mypy==0.931 mypy==0.931
pytest==6.2.5 pytest==7.0.1
pytest-asyncio==0.17.2 pytest-asyncio==0.18.1
pytest-cov==3.0.0 pytest-cov==3.0.0
pytest-mock==3.7.0 pytest-mock==3.7.0
pytest-random-order==1.0.4 pytest-random-order==1.0.4
@ -17,12 +17,12 @@ isort==5.10.1
time-machine==2.6.0 time-machine==2.6.0
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==6.4.1 nbconvert==6.4.2
# mypy types # mypy types
types-cachetools==4.2.9 types-cachetools==4.2.9
types-filelock==3.2.5 types-filelock==3.2.5
types-requests==2.27.7 types-requests==2.27.10
types-tabulate==0.8.5 types-tabulate==0.8.5
# Extensions to datetime library # Extensions to datetime library

View File

@ -2,9 +2,9 @@
-r requirements.txt -r requirements.txt
# Required for hyperopt # Required for hyperopt
scipy==1.7.3 scipy==1.8.0
scikit-learn==1.0.2 scikit-learn==1.0.2
scikit-optimize==0.9.0 scikit-optimize==0.9.0
filelock==3.4.2 filelock==3.6.0
joblib==1.1.0 joblib==1.1.0
progressbar2==4.0.0 progressbar2==4.0.0

View File

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

View File

@ -1,13 +1,13 @@
numpy==1.22.1 numpy==1.22.2
pandas==1.4.0 pandas==1.4.1
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==1.71.73 ccxt==1.73.70
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==36.0.1 cryptography==36.0.1
aiohttp==3.8.1 aiohttp==3.8.1
SQLAlchemy==1.4.31 SQLAlchemy==1.4.31
python-telegram-bot==13.10 python-telegram-bot==13.11
arrow==1.2.2 arrow==1.2.2
cachetools==4.2.2 cachetools==4.2.2
requests==2.27.1 requests==2.27.1
@ -25,14 +25,14 @@ blosc==1.10.6
py_find_1st==1.1.5 py_find_1st==1.1.5
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.5 python-rapidjson==1.6
# Notify systemd # Notify systemd
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.73.0 fastapi==0.74.0
uvicorn==0.17.1 uvicorn==0.17.5
pyjwt==2.3.0 pyjwt==2.3.0
aiofiles==0.8.0 aiofiles==0.8.0
psutil==5.9.0 psutil==5.9.0
@ -41,6 +41,6 @@ psutil==5.9.0
colorama==0.4.4 colorama==0.4.4
# Building config files interactively # Building config files interactively
questionary==1.10.0 questionary==1.10.0
prompt-toolkit==3.0.26 prompt-toolkit==3.0.28
# Extensions to datetime library # Extensions to datetime library
python-dateutil==2.8.2 python-dateutil==2.8.2

View File

@ -36,7 +36,7 @@ function check_installed_python() {
fi fi
done done
echo "No usable python found. Please make sure to have python3.7 or newer installed." echo "No usable python found. Please make sure to have python3.8 or newer installed."
exit 1 exit 1
} }

View File

@ -19,13 +19,14 @@ from freqtrade.edge import PairInfo
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.persistence import LocalTrade, Order, Trade, init_db
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver
from freqtrade.worker import Worker from freqtrade.worker import Worker
from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4,
mock_trade_5, mock_trade_6) mock_trade_5, mock_trade_6)
from tests.conftest_trades_usdt import (mock_trade_usdt_1, mock_trade_usdt_2, mock_trade_usdt_3, from tests.conftest_trades_usdt import (mock_trade_usdt_1, mock_trade_usdt_2, mock_trade_usdt_3,
mock_trade_usdt_4, mock_trade_usdt_5, mock_trade_usdt_6) mock_trade_usdt_4, mock_trade_usdt_5, mock_trade_usdt_6,
mock_trade_usdt_7)
logging.getLogger('').setLevel(logging.INFO) logging.getLogger('').setLevel(logging.INFO)
@ -200,6 +201,9 @@ def create_mock_trades(fee, use_db: bool = True):
""" """
Create some fake trades ... Create some fake trades ...
""" """
if use_db:
Trade.query.session.rollback()
def add_trade(trade): def add_trade(trade):
if use_db: if use_db:
Trade.query.session.add(trade) Trade.query.session.add(trade)
@ -258,6 +262,8 @@ def create_mock_trades_usdt(fee, use_db: bool = True):
trade = mock_trade_usdt_6(fee) trade = mock_trade_usdt_6(fee)
add_trade(trade) add_trade(trade)
trade = mock_trade_usdt_7(fee)
add_trade(trade)
if use_db: if use_db:
Trade.commit() Trade.commit()
@ -1218,7 +1224,7 @@ def limit_sell_order_open():
'id': 'mocked_limit_sell', 'id': 'mocked_limit_sell',
'type': 'limit', 'type': 'limit',
'side': 'sell', 'side': 'sell',
'pair': 'mocked', 'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'timestamp': arrow.utcnow().int_timestamp, 'timestamp': arrow.utcnow().int_timestamp,
'price': 0.00001173, 'price': 0.00001173,
@ -1982,7 +1988,7 @@ def import_fails() -> None:
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def open_trade(): def open_trade():
return Trade( trade = Trade(
pair='ETH/BTC', pair='ETH/BTC',
open_rate=0.00001099, open_rate=0.00001099,
exchange='binance', exchange='binance',
@ -1994,6 +2000,26 @@ def open_trade():
open_date=arrow.utcnow().shift(minutes=-601).datetime, open_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=True is_open=True
) )
trade.orders = [
Order(
ft_order_side='buy',
ft_pair=trade.pair,
ft_is_open=False,
order_id='123456789',
status="closed",
symbol=trade.pair,
order_type="market",
side="buy",
price=trade.open_rate,
average=trade.open_rate,
filled=trade.amount,
remaining=0,
cost=trade.open_rate * trade.amount,
order_date=trade.open_date,
order_filled_date=trade.open_date,
)
]
return trade
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
@ -2185,7 +2211,7 @@ def limit_sell_order_usdt_open():
'id': 'mocked_limit_sell_usdt', 'id': 'mocked_limit_sell_usdt',
'type': 'limit', 'type': 'limit',
'side': 'sell', 'side': 'sell',
'pair': 'mocked', 'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'timestamp': arrow.utcnow().int_timestamp, 'timestamp': arrow.utcnow().int_timestamp,
'price': 2.20, 'price': 2.20,

View File

@ -14,6 +14,7 @@ def mock_order_1():
'side': 'buy', 'side': 'buy',
'type': 'limit', 'type': 'limit',
'price': 0.123, 'price': 0.123,
'average': 0.123,
'amount': 123.0, 'amount': 123.0,
'filled': 123.0, 'filled': 123.0,
'remaining': 0.0, 'remaining': 0.0,

View File

@ -303,3 +303,61 @@ def mock_trade_usdt_6(fee):
o = Order.parse_from_ccxt_object(mock_order_usdt_6_sell(), 'LTC/USDT', 'sell') o = Order.parse_from_ccxt_object(mock_order_usdt_6_sell(), 'LTC/USDT', 'sell')
trade.orders.append(o) trade.orders.append(o)
return trade return trade
def mock_order_usdt_7():
return {
'id': 'prod_buy_7',
'symbol': 'LTC/USDT',
'status': 'closed',
'side': 'buy',
'type': 'limit',
'price': 10.0,
'amount': 2.0,
'filled': 2.0,
'remaining': 0.0,
}
def mock_order_usdt_7_sell():
return {
'id': 'prod_sell_7',
'symbol': 'LTC/USDT',
'status': 'closed',
'side': 'sell',
'type': 'limit',
'price': 8.0,
'amount': 2.0,
'filled': 2.0,
'remaining': 0.0,
}
def mock_trade_usdt_7(fee):
"""
Simulate prod entry with open sell order
"""
trade = Trade(
pair='LTC/USDT',
stake_amount=20.0,
amount=2.0,
amount_requested=2.0,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5),
fee_open=fee.return_value,
fee_close=fee.return_value,
is_open=False,
open_rate=10.0,
close_rate=8.0,
close_profit=-0.2,
close_profit_abs=-4.0,
exchange='binance',
strategy='SampleStrategy',
open_order_id="prod_sell_6",
timeframe=5,
)
o = Order.parse_from_ccxt_object(mock_order_usdt_7(), 'LTC/USDT', 'buy')
trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_usdt_7_sell(), 'LTC/USDT', 'sell')
trade.orders.append(o)
return trade

View File

@ -53,7 +53,7 @@ EXCHANGES = {
'hasQuoteVolume': True, 'hasQuoteVolume': True,
'timeframe': '5m', 'timeframe': '5m',
}, },
'okex': { 'okx': {
'pair': 'BTC/USDT', 'pair': 'BTC/USDT',
'stake_currency': 'USDT', 'stake_currency': 'USDT',
'hasQuoteVolume': True, 'hasQuoteVolume': True,

View File

@ -125,7 +125,7 @@ def test_stoploss_adjust_ftx(mocker, default_conf):
assert not exchange.stoploss_adjust(1501, order) assert not exchange.stoploss_adjust(1501, order)
def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): def test_fetch_stoploss_order_ftx(default_conf, mocker, limit_sell_order):
default_conf['dry_run'] = True default_conf['dry_run'] = True
order = MagicMock() order = MagicMock()
order.myid = 123 order.myid = 123
@ -147,9 +147,15 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order):
with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"): with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"):
exchange.fetch_stoploss_order('X', 'TKN/BTC')['status'] exchange.fetch_stoploss_order('X', 'TKN/BTC')['status']
api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': 'closed'}]) # stoploss Limit order
api_mock.fetch_orders = MagicMock(return_value=[
{'id': 'X', 'status': 'closed',
'info': {
'orderId': 'mocked_limit_sell',
}}])
api_mock.fetch_order = MagicMock(return_value=limit_sell_order) api_mock.fetch_order = MagicMock(return_value=limit_sell_order)
# No orderId field - no call to fetch_order
resp = exchange.fetch_stoploss_order('X', 'TKN/BTC') resp = exchange.fetch_stoploss_order('X', 'TKN/BTC')
assert resp assert resp
assert api_mock.fetch_order.call_count == 1 assert api_mock.fetch_order.call_count == 1
@ -158,6 +164,17 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order):
assert resp['type'] == 'stop' assert resp['type'] == 'stop'
assert resp['status_stop'] == 'triggered' assert resp['status_stop'] == 'triggered'
# Stoploss market order
# Contains no new Order, but "average" instead
order = {'id': 'X', 'status': 'closed', 'info': {'orderId': None}, 'average': 0.254}
api_mock.fetch_orders = MagicMock(return_value=[order])
api_mock.fetch_order.reset_mock()
resp = exchange.fetch_stoploss_order('X', 'TKN/BTC')
assert resp
# fetch_order not called (no regular order ID)
assert api_mock.fetch_order.call_count == 0
assert order == order
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')

View File

@ -36,6 +36,8 @@ class BTContainer(NamedTuple):
trailing_stop_positive_offset: float = 0.0 trailing_stop_positive_offset: float = 0.0
use_sell_signal: bool = False use_sell_signal: bool = False
use_custom_stoploss: bool = False use_custom_stoploss: bool = False
custom_entry_price: Optional[float] = None
custom_exit_price: Optional[float] = None
def _get_frame_time_from_offset(offset): def _get_frame_time_from_offset(offset):

View File

@ -1,5 +1,6 @@
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, C0330, unused-argument # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, C0330, unused-argument
import logging import logging
from unittest.mock import MagicMock
import pytest import pytest
@ -534,6 +535,94 @@ tc33 = BTContainer(data=[
)] )]
) )
# Test 34: Custom-entry-price below all candles should timeout - so no trade happens.
tc34 = BTContainer(data=[
# D O H L C V B S
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5500, 4951, 5000, 6172, 0, 0], # timeout
[2, 4900, 5250, 4500, 5100, 6172, 0, 0],
[3, 5100, 5100, 4650, 4750, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=0.0,
custom_entry_price=4200, trades=[]
)
# Test 35: Custom-entry-price above all candles should have rate adjusted to "entry candle high"
tc35 = BTContainer(data=[
# D O H L C V B S
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5500, 4951, 5000, 6172, 0, 0], # Timeout
[2, 4900, 5250, 4500, 5100, 6172, 0, 0],
[3, 5100, 5100, 4650, 4750, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01,
custom_entry_price=7200, trades=[
BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)
]
)
# Test 36: Custom-entry-price around candle low
# Would cause immediate ROI exit, but since the trade was entered
# below open, we treat this as cheating, and delay the sell by 1 candle.
# details: https://github.com/freqtrade/freqtrade/issues/6261
tc36 = BTContainer(data=[
# D O H L C V B S BT
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5500, 4951, 4999, 6172, 0, 0], # Enter and immediate ROI
[2, 4900, 5250, 4500, 5100, 6172, 0, 0],
[3, 5100, 5100, 4650, 4750, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01,
custom_entry_price=4952,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)]
)
# Test 37: Custom-entry-price around candle low
# Would cause immediate ROI exit below close
# details: https://github.com/freqtrade/freqtrade/issues/6261
tc37 = BTContainer(data=[
# D O H L C V B S BT
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5400, 5500, 4951, 5100, 6172, 0, 0], # Enter and immediate ROI
[2, 4900, 5250, 4500, 5100, 6172, 0, 0],
[3, 5100, 5100, 4650, 4750, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01,
custom_entry_price=4952,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)]
)
# Test 38: Custom exit price below all candles
# Price adjusted to candle Low.
tc38 = BTContainer(data=[
# D O H L C V B S BT
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5500, 4951, 5000, 6172, 0, 0],
[2, 4900, 5250, 4900, 5100, 6172, 0, 1], # exit - but timeout
[3, 5100, 5100, 4950, 4950, 6172, 0, 0],
[4, 5000, 5100, 4950, 4950, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.01,
use_sell_signal=True,
custom_exit_price=4552,
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=3)]
)
# Test 39: Custom exit price above all candles
# causes sell signal timeout
tc39 = BTContainer(data=[
# D O H L C V B S BT
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5500, 4951, 5000, 6172, 0, 0],
[2, 4900, 5250, 4900, 5100, 6172, 0, 1], # exit - but timeout
[3, 5100, 5100, 4950, 4950, 6172, 0, 0],
[4, 5000, 5100, 4950, 4950, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0,
use_sell_signal=True,
custom_exit_price=6052,
trades=[BTrade(sell_reason=SellType.FORCE_SELL, open_tick=1, close_tick=4)]
)
TESTS = [ TESTS = [
tc0, tc0,
tc1, tc1,
@ -569,6 +658,12 @@ TESTS = [
tc31, tc31,
tc32, tc32,
tc33, tc33,
tc34,
tc35,
tc36,
tc37,
tc38,
tc39,
] ]
@ -597,6 +692,10 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
backtesting.required_startup = 0 backtesting.required_startup = 0
backtesting.strategy.advise_buy = lambda a, m: frame backtesting.strategy.advise_buy = lambda a, m: frame
backtesting.strategy.advise_sell = lambda a, m: frame backtesting.strategy.advise_sell = lambda a, m: frame
if data.custom_entry_price:
backtesting.strategy.custom_entry_price = MagicMock(return_value=data.custom_entry_price)
if data.custom_exit_price:
backtesting.strategy.custom_exit_price = MagicMock(return_value=data.custom_exit_price)
backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)

View File

@ -21,6 +21,7 @@ from freqtrade.data.dataprovider import DataProvider
from freqtrade.data.history import get_timerange from freqtrade.data.history import get_timerange
from freqtrade.enums import RunMode, SellType from freqtrade.enums import RunMode, SellType
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange.exchange import timeframe_to_next_date
from freqtrade.misc import get_strategy_run_id from freqtrade.misc import get_strategy_run_id
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
from freqtrade.persistence import LocalTrade from freqtrade.persistence import LocalTrade
@ -51,6 +52,13 @@ def trim_dictlist(dict_list, num):
return new return new
@pytest.fixture(autouse=True)
def backtesting_cleanup() -> None:
yield None
Backtesting.cleanup()
def load_data_test(what, testdatadir): def load_data_test(what, testdatadir):
timerange = TimeRange.parse_timerange('1510694220-1510700340') timerange = TimeRange.parse_timerange('1510694220-1510700340')
data = history.load_pair_history(pair='UNITTEST/BTC', datadir=testdatadir, data = history.load_pair_history(pair='UNITTEST/BTC', datadir=testdatadir,
@ -520,6 +528,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
# Fake 2 trades, so there's not enough amount for the next trade left. # Fake 2 trades, so there's not enough amount for the next trade left.
LocalTrade.trades_open.append(trade) LocalTrade.trades_open.append(trade)
LocalTrade.trades_open.append(trade) LocalTrade.trades_open.append(trade)
backtesting.wallets.update()
trade = backtesting._enter_trade(pair, row=row) trade = backtesting._enter_trade(pair, row=row)
assert trade is None assert trade is None
LocalTrade.trades_open.pop() LocalTrade.trades_open.pop()
@ -527,6 +536,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
assert trade is not None assert trade is not None
backtesting.strategy.custom_stake_amount = lambda **kwargs: 123.5 backtesting.strategy.custom_stake_amount = lambda **kwargs: 123.5
backtesting.wallets.update()
trade = backtesting._enter_trade(pair, row=row) trade = backtesting._enter_trade(pair, row=row)
assert trade assert trade
assert trade.stake_amount == 123.5 assert trade.stake_amount == 123.5
@ -550,8 +560,6 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
trade = backtesting._enter_trade(pair, row=row) trade = backtesting._enter_trade(pair, row=row)
assert trade is None assert trade is None
backtesting.cleanup()
def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
default_conf['use_sell_signal'] = False default_conf['use_sell_signal'] = False
@ -634,7 +642,8 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
assert res.sell_reason == SellType.ROI.value assert res.sell_reason == SellType.ROI.value
# Sell at minute 3 (not available above!) # Sell at minute 3 (not available above!)
assert res.close_date_utc == datetime(2020, 1, 1, 5, 3, tzinfo=timezone.utc) assert res.close_date_utc == datetime(2020, 1, 1, 5, 3, tzinfo=timezone.utc)
assert round(res.close_rate, 3) == round(209.0225, 3) sell_order = res.select_order('sell', True)
assert sell_order is not None
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
@ -650,6 +659,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
timerange=timerange) timerange=timerange)
processed = backtesting.strategy.advise_all_indicators(data) processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
result = backtesting.backtest( result = backtesting.backtest(
processed=deepcopy(processed), processed=deepcopy(processed),
start_date=min_date, start_date=min_date,
@ -741,6 +751,46 @@ def test_processed(default_conf, mocker, testdatadir) -> None:
assert col in cols assert col in cols
def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadir) -> None:
default_conf['use_sell_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
patch_exchange(mocker)
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
timerange = TimeRange('date', None, 1517227800, 0)
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
timerange=timerange)
processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed)
global count
count = 0
def tmp_confirm_entry(pair, current_time, **kwargs):
dp = backtesting.strategy.dp
df, _ = dp.get_analyzed_dataframe(pair, backtesting.strategy.timeframe)
current_candle = df.iloc[-1].squeeze()
assert current_candle['buy'] == 1
candle_date = timeframe_to_next_date(backtesting.strategy.timeframe, current_candle['date'])
assert candle_date == current_time
# These asserts don't properly raise as they are nested,
# therefore we increment count and assert for that.
global count
count = count + 1
backtesting.strategy.confirm_trade_entry = tmp_confirm_entry
backtesting.backtest(
processed=deepcopy(processed),
start_date=min_date,
end_date=max_date,
max_open_trades=10,
position_stacking=False,
)
assert count == 5
def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatadir) -> None: def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatadir) -> None:
# While this test IS a copy of test_backtest_pricecontours, it's needed to ensure # While this test IS a copy of test_backtest_pricecontours, it's needed to ensure
# results do not carry-over to the next run, which is not given by using parametrize. # results do not carry-over to the next run, which is not given by using parametrize.
@ -978,6 +1028,8 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
'config': default_conf, 'config': default_conf,
'locks': [], 'locks': [],
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0,
'timedout_exit_orders': 0,
'final_balance': 1000, 'final_balance': 1000,
}) })
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
@ -1086,6 +1138,8 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'config': default_conf, 'config': default_conf,
'locks': [], 'locks': [],
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0,
'timedout_exit_orders': 0,
'final_balance': 1000, 'final_balance': 1000,
}, },
{ {
@ -1093,6 +1147,8 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'config': default_conf, 'config': default_conf,
'locks': [], 'locks': [],
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0,
'timedout_exit_orders': 0,
'final_balance': 1000, 'final_balance': 1000,
} }
]) ])
@ -1195,6 +1251,8 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
'config': default_conf, 'config': default_conf,
'locks': [], 'locks': [],
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0,
'timedout_exit_orders': 0,
'final_balance': 1000, 'final_balance': 1000,
}, },
{ {
@ -1202,6 +1260,8 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
'config': default_conf, 'config': default_conf,
'locks': [], 'locks': [],
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0,
'timedout_exit_orders': 0,
'final_balance': 1000, 'final_balance': 1000,
} }
]) ])
@ -1263,6 +1323,8 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda
'config': default_conf, 'config': default_conf,
'locks': [], 'locks': [],
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0,
'timedout_exit_orders': 0,
'final_balance': 1000, 'final_balance': 1000,
}) })
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',

View File

@ -364,6 +364,8 @@ def test_hyperopt_format_results(hyperopt):
'locks': [], 'locks': [],
'final_balance': 0.02, 'final_balance': 0.02,
'rejected_signals': 2, 'rejected_signals': 2,
'timedout_entry_orders': 0,
'timedout_exit_orders': 0,
'backtest_start_time': 1619718665, 'backtest_start_time': 1619718665,
'backtest_end_time': 1619718665, 'backtest_end_time': 1619718665,
} }
@ -431,6 +433,8 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
'config': hyperopt_conf, 'config': hyperopt_conf,
'locks': [], 'locks': [],
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0,
'timedout_exit_orders': 0,
'final_balance': 1000, 'final_balance': 1000,
} }

View File

@ -86,6 +86,7 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) ->
"SharpeHyperOptLossDaily", "SharpeHyperOptLossDaily",
"MaxDrawDownHyperOptLoss", "MaxDrawDownHyperOptLoss",
"CalmarHyperOptLoss", "CalmarHyperOptLoss",
"ProfitDrawDownHyperOptLoss",
]) ])
def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunction) -> None: def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunction) -> None:

View File

@ -82,6 +82,8 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
'locks': [], 'locks': [],
'final_balance': 1000.02, 'final_balance': 1000.02,
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0,
'timedout_exit_orders': 0,
'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_start_time': Arrow.utcnow().int_timestamp,
'backtest_end_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp,
'run_id': '123', 'run_id': '123',
@ -131,6 +133,8 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
'locks': [], 'locks': [],
'final_balance': 1000.02, 'final_balance': 1000.02,
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0,
'timedout_exit_orders': 0,
'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_start_time': Arrow.utcnow().int_timestamp,
'backtest_end_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp,
'run_id': '124', 'run_id': '124',

View File

@ -15,7 +15,7 @@ from freqtrade.persistence import Trade
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.pairlistmanager import PairListManager
from freqtrade.resolvers import PairListResolver from freqtrade.resolvers import PairListResolver
from tests.conftest import (create_mock_trades, get_patched_exchange, get_patched_freqtradebot, from tests.conftest import (create_mock_trades_usdt, get_patched_exchange, get_patched_freqtradebot,
log_has, log_has_re, num_log_has) log_has, log_has_re, num_log_has)
@ -715,29 +715,58 @@ def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None:
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee, caplog) -> None: def test_PerformanceFilter_lookback(mocker, default_conf_usdt, fee, caplog) -> None:
whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC') default_conf_usdt['exchange']['pair_whitelist'].extend(['ADA/USDT', 'XRP/USDT', 'ETC/USDT'])
whitelist_conf['pairlists'] = [ default_conf_usdt['pairlists'] = [
{"method": "StaticPairList"}, {"method": "StaticPairList"},
{"method": "PerformanceFilter", "minutes": 60, "min_profit": 0.01} {"method": "PerformanceFilter", "minutes": 60, "min_profit": 0.01}
] ]
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
exchange = get_patched_exchange(mocker, whitelist_conf) exchange = get_patched_exchange(mocker, default_conf_usdt)
pm = PairListManager(exchange, whitelist_conf) pm = PairListManager(exchange, default_conf_usdt)
pm.refresh_pairlist() pm.refresh_pairlist()
assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC'] assert pm.whitelist == ['ETH/USDT', 'XRP/USDT', 'NEO/USDT', 'TKN/USDT']
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
create_mock_trades(fee) create_mock_trades_usdt(fee)
pm.refresh_pairlist() pm.refresh_pairlist()
assert pm.whitelist == ['XRP/BTC'] assert pm.whitelist == ['XRP/USDT']
assert log_has_re(r'Removing pair .* since .* is below .*', caplog) assert log_has_re(r'Removing pair .* since .* is below .*', caplog)
# Move to "outside" of lookback window, so original sorting is restored. # Move to "outside" of lookback window, so original sorting is restored.
t.move_to("2021-09-01 07:00:00 +00:00") t.move_to("2021-09-01 07:00:00 +00:00")
pm.refresh_pairlist() pm.refresh_pairlist()
assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC'] assert pm.whitelist == ['ETH/USDT', 'XRP/USDT', 'NEO/USDT', 'TKN/USDT']
@pytest.mark.usefixtures("init_persistence")
def test_PerformanceFilter_keep_mid_order(mocker, default_conf_usdt, fee, caplog) -> None:
default_conf_usdt['exchange']['pair_whitelist'].extend(['ADA/USDT', 'ETC/USDT'])
default_conf_usdt['pairlists'] = [
{"method": "StaticPairList", "allow_inactive": True},
{"method": "PerformanceFilter", "minutes": 60, }
]
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
exchange = get_patched_exchange(mocker, default_conf_usdt)
pm = PairListManager(exchange, default_conf_usdt)
pm.refresh_pairlist()
assert pm.whitelist == ['ETH/USDT', 'LTC/USDT', 'XRP/USDT',
'NEO/USDT', 'TKN/USDT', 'ADA/USDT', 'ETC/USDT']
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
create_mock_trades_usdt(fee)
pm.refresh_pairlist()
assert pm.whitelist == ['XRP/USDT', 'ETC/USDT', 'ETH/USDT',
'NEO/USDT', 'TKN/USDT', 'ADA/USDT', 'LTC/USDT']
# assert log_has_re(r'Removing pair .* since .* is below .*', caplog)
# Move to "outside" of lookback window, so original sorting is restored.
t.move_to("2021-09-01 07:00:00 +00:00")
pm.refresh_pairlist()
assert pm.whitelist == ['ETH/USDT', 'LTC/USDT', 'XRP/USDT',
'NEO/USDT', 'TKN/USDT', 'ADA/USDT', 'ETC/USDT']
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
@ -1168,13 +1197,13 @@ def test_pairlistmanager_no_pairlist(mocker, whitelist_conf):
{'pair': 'TKN/BTC', 'profit_ratio': -0.0501, 'count': 2}, {'pair': 'TKN/BTC', 'profit_ratio': -0.0501, 'count': 2},
{'pair': 'ETH/BTC', 'profit_ratio': -0.0501, 'count': 100}], {'pair': 'ETH/BTC', 'profit_ratio': -0.0501, 'count': 100}],
['TKN/BTC', 'ETH/BTC', 'LTC/BTC']), ['TKN/BTC', 'ETH/BTC', 'LTC/BTC']),
# Tie in performance and count, broken by alphabetical sort # Tie in performance and count, broken by prior sorting sort
([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}],
['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'],
[{'pair': 'LTC/BTC', 'profit_ratio': -0.0501, 'count': 1}, [{'pair': 'LTC/BTC', 'profit_ratio': -0.0501, 'count': 1},
{'pair': 'TKN/BTC', 'profit_ratio': -0.0501, 'count': 1}, {'pair': 'TKN/BTC', 'profit_ratio': -0.0501, 'count': 1},
{'pair': 'ETH/BTC', 'profit_ratio': -0.0501, 'count': 1}], {'pair': 'ETH/BTC', 'profit_ratio': -0.0501, 'count': 1}],
['ETH/BTC', 'LTC/BTC', 'TKN/BTC']), ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']),
]) ])
def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, overall_performance, def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, overall_performance,
allowlist_result, tickers, markets, ohlcv_history_list): allowlist_result, tickers, markets, ohlcv_history_list):

View File

@ -11,6 +11,7 @@ from freqtrade.edge import PairInfo
from freqtrade.enums import State from freqtrade.enums import State
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.persistence.models import Order
from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.pairlock_middleware import PairLocks
from freqtrade.rpc import RPC, RPCException from freqtrade.rpc import RPC, RPCException
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
@ -108,6 +109,14 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stoploss_entry_dist_ratio': -0.10448878, 'stoploss_entry_dist_ratio': -0.10448878,
'open_order': None, 'open_order': None,
'exchange': 'binance', 'exchange': 'binance',
'filled_entry_orders': [{
'amount': 91.07468123, 'average': 1.098e-05,
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
'is_open': False, 'pair': 'ETH/BTC',
'remaining': ANY, 'status': ANY}],
'filled_exit_orders': []
} }
mocker.patch('freqtrade.exchange.Exchange.get_rate', mocker.patch('freqtrade.exchange.Exchange.get_rate',
@ -175,6 +184,14 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stoploss_entry_dist_ratio': -0.10448878, 'stoploss_entry_dist_ratio': -0.10448878,
'open_order': None, 'open_order': None,
'exchange': 'binance', 'exchange': 'binance',
'filled_entry_orders': [{
'amount': 91.07468123, 'average': 1.098e-05,
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
'is_open': False, 'pair': 'ETH/BTC',
'remaining': ANY, 'status': ANY}],
'filled_exit_orders': []
} }
@ -223,7 +240,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
rpc._config['position_adjustment_enable'] = True rpc._config['position_adjustment_enable'] = True
rpc._config['max_entry_position_adjustment'] = 3 rpc._config['max_entry_position_adjustment'] = 3
result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
assert "# Buys" in headers assert "# Entries" in headers
assert len(result[0]) == 5 assert len(result[0]) == 5
# 4th column should be 1/4 - as 1 order filled (a total of 4 is possible) # 4th column should be 1/4 - as 1 order filled (a total of 4 is possible)
# 3 on top of the initial one. # 3 on top of the initial one.
@ -261,8 +278,10 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
assert trade assert trade
# Simulate buy & sell # Simulate buy & sell
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update(limit_sell_order) trade.update_trade(oobj)
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -399,28 +418,32 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
freqtradebot.enter_positions() freqtradebot.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'sell')
trade.update_trade(oobj)
# Update the ticker with a market going up # Update the ticker with a market going up
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker_sell_up fetch_ticker=ticker_sell_up
) )
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
freqtradebot.enter_positions() freqtradebot.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Update the ticker with a market going up # Update the ticker with a market going up
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker_sell_up fetch_ticker=ticker_sell_up
) )
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -479,14 +502,16 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
freqtradebot.enter_positions() freqtradebot.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Update the ticker with a market going up # Update the ticker with a market going up
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker_sell_up, fetch_ticker=ticker_sell_up,
get_fee=fee get_fee=fee
) )
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -738,13 +763,13 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
mocker.patch( mocker.patch(
'freqtrade.exchange.Exchange.fetch_order', 'freqtrade.exchange.Exchange.fetch_order',
side_effect=[{ side_effect=[{
'id': '1234', 'id': trade.orders[0].order_id,
'status': 'open', 'status': 'open',
'type': 'limit', 'type': 'limit',
'side': 'buy', 'side': 'buy',
'filled': filled_amount 'filled': filled_amount
}, { }, {
'id': '1234', 'id': trade.orders[0].order_id,
'status': 'closed', 'status': 'closed',
'type': 'limit', 'type': 'limit',
'side': 'buy', 'side': 'buy',
@ -824,10 +849,12 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -858,10 +885,12 @@ def test_buy_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -930,10 +959,12 @@ def test_sell_reason_performance_handle(default_conf, ticker, limit_buy_order, f
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -1002,10 +1033,12 @@ def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -1276,3 +1309,13 @@ def test_rpc_edge_enabled(mocker, edge_conf) -> None:
assert ret[0]['Winrate'] == 0.66 assert ret[0]['Winrate'] == 0.66
assert ret[0]['Expectancy'] == 1.71 assert ret[0]['Expectancy'] == 1.71
assert ret[0]['Stoploss'] == -0.02 assert ret[0]['Stoploss'] == -0.02
def test_rpc_health(mocker, default_conf) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc = RPC(freqtradebot)
result = rpc._health()
assert result['last_process'] == '1970-01-01 00:00:00+00:00'
assert result['last_process_ts'] == 0

View File

@ -7,6 +7,7 @@ from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from unittest.mock import ANY, MagicMock, PropertyMock from unittest.mock import ANY, MagicMock, PropertyMock
import pandas as pd
import pytest import pytest
import uvicorn import uvicorn
from fastapi import FastAPI from fastapi import FastAPI
@ -1107,6 +1108,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets):
data='{"tradeid": "1"}') data='{"tradeid": "1"}')
assert_response(rc, 502) assert_response(rc, 502)
assert rc.json() == {"error": "Error querying /api/v1/forcesell: invalid argument"} assert rc.json() == {"error": "Error querying /api/v1/forcesell: invalid argument"}
Trade.query.session.rollback()
ftbot.enter_positions() ftbot.enter_positions()
@ -1181,6 +1183,24 @@ def test_api_pair_candles(botclient, ohlcv_history):
0.7039405, 8.885e-05, 0, 0, 1511686800000, None, None] 0.7039405, 8.885e-05, 0, 0, 1511686800000, None, None]
]) ])
ohlcv_history['sell'] = ohlcv_history['sell'].astype('float64')
ohlcv_history.at[0, 'sell'] = float('inf')
ohlcv_history['date1'] = ohlcv_history['date']
ohlcv_history.at[0, 'date1'] = pd.NaT
ftbot.dataprovider._set_cached_df("XRP/BTC", timeframe, ohlcv_history)
rc = client_get(client,
f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}")
assert_response(rc)
assert (rc.json()['data'] ==
[['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869,
None, 0, None, None, 1511686200000, None, None],
['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05,
8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0.0, '2017-11-26 08:55:00',
1511686500000, 8.893e-05, None],
['2017-11-26 09:00:00', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05,
0.7039405, 8.885e-05, 0, 0.0, '2017-11-26 09:00:00', 1511686800000, None, None]
])
def test_api_pair_history(botclient, ohlcv_history): def test_api_pair_history(botclient, ohlcv_history):
@ -1330,6 +1350,11 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
ftbot, client = botclient ftbot, client = botclient
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
rc = client_get(client, f"{BASE_URI}/backtest")
# Backtest prevented in default mode
assert_response(rc, 502)
ftbot.config['runmode'] = RunMode.WEBSERVER
# Backtesting not started yet # Backtesting not started yet
rc = client_get(client, f"{BASE_URI}/backtest") rc = client_get(client, f"{BASE_URI}/backtest")
assert_response(rc) assert_response(rc)
@ -1442,3 +1467,14 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
assert result['status'] == 'reset' assert result['status'] == 'reset'
assert not result['running'] assert not result['running']
assert result['status_msg'] == 'Backtest reset' assert result['status_msg'] == 'Backtest reset'
def test_health(botclient):
ftbot, client = botclient
rc = client_get(client, f"{BASE_URI}/health")
assert_response(rc)
ret = rc.json()
assert ret['last_process_ts'] == 0
assert ret['last_process'] == '1970-01-01T00:00:00+00:00'

View File

@ -23,6 +23,7 @@ from freqtrade.exceptions import OperationalException
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.loggers import setup_logging from freqtrade.loggers import setup_logging
from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence import PairLocks, Trade
from freqtrade.persistence.models import Order
from freqtrade.rpc import RPC from freqtrade.rpc import RPC
from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.rpc import RPCException
from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.rpc.telegram import Telegram, authorized_only
@ -99,7 +100,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
"['count'], ['locks'], ['unlock', 'delete_locks'], " "['count'], ['locks'], ['unlock', 'delete_locks'], "
"['reload_config', 'reload_conf'], ['show_config', 'show_conf'], " "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], "
"['stopbuy'], ['whitelist'], ['blacklist'], ['blacklist_delete', 'bl_delete'], " "['stopbuy'], ['whitelist'], ['blacklist'], ['blacklist_delete', 'bl_delete'], "
"['logs'], ['edge'], ['help'], ['version']" "['logs'], ['edge'], ['health'], ['help'], ['version']"
"]") "]")
assert log_has(message_str, caplog) assert log_has(message_str, caplog)
@ -201,7 +202,8 @@ def test_telegram_status(default_conf, update, mocker) -> None:
'stoploss_current_dist_ratio': -0.0002, 'stoploss_current_dist_ratio': -0.0002,
'stop_loss_ratio': -0.0001, 'stop_loss_ratio': -0.0001,
'open_order': '(limit buy rem=0.00000000)', 'open_order': '(limit buy rem=0.00000000)',
'is_open': True 'is_open': True,
'filled_entry_orders': []
}]), }]),
) )
@ -217,6 +219,80 @@ def test_telegram_status(default_conf, update, mocker) -> None:
assert status_table.call_count == 1 assert status_table.call_count == 1
@pytest.mark.usefixtures("init_persistence")
def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
update.message.chat.id = "123"
default_conf['telegram']['enabled'] = False
default_conf['telegram']['chat_id'] = "123"
default_conf['position_adjustment_enable'] = True
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_order=MagicMock(return_value=None),
get_rate=MagicMock(return_value=0.22),
)
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
create_mock_trades(fee)
trades = Trade.get_open_trades()
trade = trades[0]
trade.orders.append(Order(
order_id='5412vbb',
ft_order_side='buy',
ft_pair=trade.pair,
ft_is_open=False,
status="closed",
symbol=trade.pair,
order_type="market",
side="buy",
price=trade.open_rate * 0.95,
average=trade.open_rate * 0.95,
filled=trade.amount,
remaining=0,
cost=trade.amount,
order_date=trade.open_date,
order_filled_date=trade.open_date,
)
)
trade.recalc_trade_from_orders()
Trade.commit()
telegram._status(update=update, context=MagicMock())
assert msg_mock.call_count == 4
msg = msg_mock.call_args_list[0][0][0]
assert re.search(r'Number of Entries.*2', msg)
assert re.search(r'Average Entry Price', msg)
assert re.search(r'Order filled at', msg)
assert re.search(r'Close Date:', msg) is None
assert re.search(r'Close Profit:', msg) is None
@pytest.mark.usefixtures("init_persistence")
def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None:
update.message.chat.id = "123"
default_conf['telegram']['enabled'] = False
default_conf['telegram']['chat_id'] = "123"
default_conf['position_adjustment_enable'] = True
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_order=MagicMock(return_value=None),
get_rate=MagicMock(return_value=0.22),
)
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
create_mock_trades(fee)
trades = Trade.get_trades([Trade.is_open.is_(False)])
trade = trades[0]
context = MagicMock()
context.args = [str(trade.id)]
telegram._status(update=update, context=context)
assert msg_mock.call_count == 1
msg = msg_mock.call_args_list[0][0][0]
assert re.search(r'Close Date:', msg)
assert re.search(r'Close Profit:', msg)
def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
default_conf['max_open_trades'] = 3 default_conf['max_open_trades'] = 3
mocker.patch.multiple( mocker.patch.multiple(
@ -342,10 +418,12 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobjs)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -385,8 +463,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
trades = Trade.query.all() trades = Trade.query.all()
for trade in trades: for trade in trades:
trade.update(limit_buy_order) trade.update_trade(oobj)
trade.update(limit_sell_order) trade.update_trade(oobjs)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -451,10 +529,12 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee,
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobjs)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -498,8 +578,8 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee,
trades = Trade.query.all() trades = Trade.query.all()
for trade in trades: for trade in trades:
trade.update(limit_buy_order) trade.update_trade(oobj)
trade.update(limit_sell_order) trade.update_trade(oobjs)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -567,10 +647,12 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobjs)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -614,8 +696,8 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
trades = Trade.query.all() trades = Trade.query.all()
for trade in trades: for trade in trades:
trade.update(limit_buy_order) trade.update_trade(oobj)
trade.update(limit_sell_order) trade.update_trade(oobjs)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -685,7 +767,9 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
context = MagicMock() context = MagicMock()
# Test with invalid 2nd argument (should silently pass) # Test with invalid 2nd argument (should silently pass)
context.args = ["aaa"] context.args = ["aaa"]
@ -694,13 +778,15 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
assert 'No closed trade' in msg_mock.call_args_list[-1][0][0] assert 'No closed trade' in msg_mock.call_args_list[-1][0][0]
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0] assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=0.01) mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=0.01)
assert ('∙ `-0.00000500 BTC (-0.50%) (-0.0 \N{GREEK CAPITAL LETTER SIGMA}%)`' assert ('∙ `-0.000005 BTC (-0.50%) (-0.0 \N{GREEK CAPITAL LETTER SIGMA}%)`'
in msg_mock.call_args_list[-1][0][0]) in msg_mock.call_args_list[-1][0][0])
msg_mock.reset_mock() msg_mock.reset_mock()
# Update the ticker with a market going up # Update the ticker with a market going up
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up) mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up)
trade.update(limit_sell_order) # Simulate fulfilled LIMIT_SELL order for trade
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.now(timezone.utc) trade.close_date = datetime.now(timezone.utc)
trade.is_open = False trade.is_open = False
@ -769,7 +855,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick
assert '*XRP:*' not in result assert '*XRP:*' not in result
assert 'Balance:' in result assert 'Balance:' in result
assert 'Est. BTC:' in result assert 'Est. BTC:' in result
assert 'BTC: 12.00000000' in result assert 'BTC: 12' in result
assert "*3 Other Currencies (< 0.0001 BTC):*" in result assert "*3 Other Currencies (< 0.0001 BTC):*" in result
assert 'BTC: 0.00000309' in result assert 'BTC: 0.00000309' in result
@ -785,7 +871,7 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
telegram._balance(update=update, context=MagicMock()) telegram._balance(update=update, context=MagicMock())
result = msg_mock.call_args_list[0][0][0] result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'All balances are zero.' in result assert 'Starting capital: `0 BTC' in result
def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None:
@ -798,7 +884,7 @@ def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None
result = msg_mock.call_args_list[0][0][0] result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert "*Warning:* Simulated balances in Dry Mode." in result assert "*Warning:* Simulated balances in Dry Mode." in result
assert "Starting capital: `1000` BTC" in result assert "Starting capital: `1000 BTC`" in result
def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: def test_balance_handle_too_large_response(default_conf, update, mocker) -> None:
@ -1184,7 +1270,8 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None:
assert msg_mock.call_args_list[0][1]['msg'] == 'Which pair?' assert msg_mock.call_args_list[0][1]['msg'] == 'Which pair?'
# assert msg_mock.call_args_list[0][1]['callback_query_handler'] == 'forcebuy' # assert msg_mock.call_args_list[0][1]['callback_query_handler'] == 'forcebuy'
keyboard = msg_mock.call_args_list[0][1]['keyboard'] keyboard = msg_mock.call_args_list[0][1]['keyboard']
assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 4 # One additional button - cancel
assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5
update = MagicMock() update = MagicMock()
update.callback_query = MagicMock() update.callback_query = MagicMock()
update.callback_query.data = 'XRP/USDT' update.callback_query.data = 'XRP/USDT'
@ -1209,10 +1296,12 @@ def test_telegram_performance_handle(default_conf, update, ticker, fee,
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -1236,13 +1325,15 @@ def test_telegram_buy_tag_performance_handle(default_conf, update, ticker, fee,
freqtradebot.enter_positions() freqtradebot.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
trade.buy_tag = "TESTBUY"
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
trade.buy_tag = "TESTBUY"
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -1279,13 +1370,14 @@ def test_telegram_sell_reason_performance_handle(default_conf, update, ticker, f
freqtradebot.enter_positions() freqtradebot.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
trade.sell_reason = 'TESTSELL' trade.sell_reason = 'TESTSELL'
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -1322,15 +1414,16 @@ def test_telegram_mix_tag_performance_handle(default_conf, update, ticker, fee,
freqtradebot.enter_positions() freqtradebot.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
trade.buy_tag = "TESTBUY" trade.buy_tag = "TESTBUY"
trade.sell_reason = "TESTSELL" trade.sell_reason = "TESTSELL"
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -1657,7 +1750,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None:
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'limit': 1.099e-05, 'limit': 1.099e-05,
'order_type': 'limit', 'order_type': 'limit',
'stake_amount': 0.001, 'stake_amount': 0.01465333,
'stake_amount_fiat': 0.0, 'stake_amount_fiat': 0.0,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
@ -1674,7 +1767,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None:
'*Amount:* `1333.33333333`\n' \ '*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00001099`\n' \ '*Open Rate:* `0.00001099`\n' \
'*Current Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \
'*Total:* `(0.00100000 BTC, 12.345 USD)`' '*Total:* `(0.01465333 BTC, 180.895 USD)`'
freqtradebot.config['telegram']['notification_settings'] = {'buy': 'off'} freqtradebot.config['telegram']['notification_settings'] = {'buy': 'off'}
caplog.clear() caplog.clear()
@ -1748,7 +1841,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker) -> None:
'buy_tag': 'buy_signal_01', 'buy_tag': 'buy_signal_01',
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'stake_amount': 0.001, 'stake_amount': 0.01465333,
# 'stake_amount_fiat': 0.0, # 'stake_amount_fiat': 0.0,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
@ -1762,7 +1855,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker) -> None:
'*Buy Tag:* `buy_signal_01`\n' \ '*Buy Tag:* `buy_signal_01`\n' \
'*Amount:* `1333.33333333`\n' \ '*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00001099`\n' \ '*Open Rate:* `0.00001099`\n' \
'*Total:* `(0.00100000 BTC, 12.345 USD)`' '*Total:* `(0.01465333 BTC, 180.895 USD)`'
def test_send_msg_sell_notification(default_conf, mocker) -> None: def test_send_msg_sell_notification(default_conf, mocker) -> None:
@ -1954,7 +2047,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'limit': 1.099e-05, 'limit': 1.099e-05,
'order_type': 'limit', 'order_type': 'limit',
'stake_amount': 0.001, 'stake_amount': 0.01465333,
'stake_amount_fiat': 0.0, 'stake_amount_fiat': 0.0,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'fiat_currency': None, 'fiat_currency': None,
@ -1967,7 +2060,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00001099`\n' '*Open Rate:* `0.00001099`\n'
'*Current Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n'
'*Total:* `(0.00100000 BTC)`') '*Total:* `(0.01465333 BTC)`')
def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:

View File

@ -437,7 +437,8 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
strategy.custom_stoploss = custom_stop strategy.custom_stoploss = custom_stop
now = arrow.utcnow().datetime now = arrow.utcnow().datetime
sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade, current_rate = trade.open_rate * (1 + profit)
sl_flag = strategy.stop_loss_reached(current_rate=current_rate, trade=trade,
current_time=now, current_profit=profit, current_time=now, current_profit=profit,
force_stoploss=0, high=None) force_stoploss=0, high=None)
assert isinstance(sl_flag, SellCheckTuple) assert isinstance(sl_flag, SellCheckTuple)
@ -447,8 +448,9 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
else: else:
assert sl_flag.sell_flag is True assert sl_flag.sell_flag is True
assert round(trade.stop_loss, 2) == adjusted assert round(trade.stop_loss, 2) == adjusted
current_rate2 = trade.open_rate * (1 + profit2)
sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit2), trade=trade, sl_flag = strategy.stop_loss_reached(current_rate=current_rate2, trade=trade,
current_time=now, current_profit=profit2, current_time=now, current_profit=profit2,
force_stoploss=0, high=None) force_stoploss=0, high=None)
assert sl_flag.sell_type == expected2 assert sl_flag.sell_type == expected2

View File

@ -227,7 +227,8 @@ def test_edge_overrides_stoploss(limit_buy_order_usdt, fee, caplog, mocker,
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
freqtrade.enter_positions() freqtrade.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
############################################# #############################################
# stoploss shoud be hit # stoploss shoud be hit
@ -292,7 +293,8 @@ def test_create_trade(default_conf_usdt, ticker_usdt, limit_buy_order_usdt, fee,
assert trade.exchange == 'binance' assert trade.exchange == 'binance'
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
assert trade.open_rate == 2.0 assert trade.open_rate == 2.0
assert trade.amount == 30.0 assert trade.amount == 30.0
@ -982,11 +984,17 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog,
trade = Trade.query.first() trade = Trade.query.first()
trade.is_open = True trade.is_open = True
trade.open_order_id = None trade.open_order_id = None
trade.stoploss_order_id = 100 trade.stoploss_order_id = "100"
trade.orders.append(Order(
ft_order_side='stoploss',
order_id='100',
ft_pair=trade.pair,
ft_is_open=True,
))
assert trade assert trade
stoploss_order_hit = MagicMock(return_value={ stoploss_order_hit = MagicMock(return_value={
'id': 100, 'id': "100",
'status': 'closed', 'status': 'closed',
'type': 'stop_loss_limit', 'type': 'stop_loss_limit',
'price': 3, 'price': 3,
@ -1632,9 +1640,9 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
return_value=limit_buy_order_usdt['amount']) return_value=limit_buy_order_usdt['amount'])
order_id = limit_buy_order_usdt['id']
trade = Trade( trade = Trade(
open_order_id=123, open_order_id=order_id,
fee_open=0.001, fee_open=0.001,
fee_close=0.001, fee_close=0.001,
open_rate=0.01, open_rate=0.01,
@ -1642,29 +1650,35 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap
amount=11, amount=11,
exchange="binance", exchange="binance",
) )
trade.orders.append(Order(
ft_order_side='buy',
price=0.01,
order_id=order_id,
))
assert not freqtrade.update_trade_state(trade, None) assert not freqtrade.update_trade_state(trade, None)
assert log_has_re(r'Orderid for trade .* is empty.', caplog) assert log_has_re(r'Orderid for trade .* is empty.', caplog)
caplog.clear() caplog.clear()
# Add datetime explicitly since sqlalchemy defaults apply only once written to database # Add datetime explicitly since sqlalchemy defaults apply only once written to database
freqtrade.update_trade_state(trade, '123') freqtrade.update_trade_state(trade, order_id)
# Test amount not modified by fee-logic # Test amount not modified by fee-logic
assert not log_has_re(r'Applying fee to .*', caplog) assert not log_has_re(r'Applying fee to .*', caplog)
caplog.clear() caplog.clear()
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.amount == limit_buy_order_usdt['amount'] assert trade.amount == limit_buy_order_usdt['amount']
trade.open_order_id = '123' trade.open_order_id = order_id
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81)
assert trade.amount != 90.81 assert trade.amount != 90.81
# test amount modified by fee-logic # test amount modified by fee-logic
freqtrade.update_trade_state(trade, '123') freqtrade.update_trade_state(trade, order_id)
assert trade.amount == 90.81 assert trade.amount == 90.81
assert trade.open_order_id is None assert trade.open_order_id is None
trade.is_open = True trade.is_open = True
trade.open_order_id = None trade.open_order_id = None
# Assert we call handle_trade() if trade is feasible for execution # Assert we call handle_trade() if trade is feasible for execution
freqtrade.update_trade_state(trade, '123') freqtrade.update_trade_state(trade, order_id)
assert log_has_re('Found open order for.*', caplog) assert log_has_re('Found open order for.*', caplog)
limit_buy_order_usdt_new = deepcopy(limit_buy_order_usdt) limit_buy_order_usdt_new = deepcopy(limit_buy_order_usdt)
@ -1673,7 +1687,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', side_effect=ValueError) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', side_effect=ValueError)
mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order_usdt_new) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order_usdt_new)
res = freqtrade.update_trade_state(trade, '123') res = freqtrade.update_trade_state(trade, order_id)
# Cancelled empty # Cancelled empty
assert res is True assert res is True
@ -1685,6 +1699,8 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap
def test_update_trade_state_withorderdict(default_conf_usdt, trades_for_order, limit_buy_order_usdt, def test_update_trade_state_withorderdict(default_conf_usdt, trades_for_order, limit_buy_order_usdt,
fee, mocker, initial_amount, has_rounding_fee, caplog): fee, mocker, initial_amount, has_rounding_fee, caplog):
trades_for_order[0]['amount'] = initial_amount trades_for_order[0]['amount'] = initial_amount
order_id = "oid_123456"
limit_buy_order_usdt['id'] = order_id
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
# fetch_order should not be called!! # fetch_order should not be called!!
mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
@ -1700,14 +1716,26 @@ def test_update_trade_state_withorderdict(default_conf_usdt, trades_for_order, l
open_date=arrow.utcnow().datetime, open_date=arrow.utcnow().datetime,
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
open_order_id="123456", open_order_id=order_id,
is_open=True, is_open=True,
) )
freqtrade.update_trade_state(trade, '123456', limit_buy_order_usdt) trade.orders.append(
Order(
ft_order_side='buy',
ft_pair=trade.pair,
ft_is_open=True,
order_id=order_id,
)
)
freqtrade.update_trade_state(trade, order_id, limit_buy_order_usdt)
assert trade.amount != amount assert trade.amount != amount
assert trade.amount == limit_buy_order_usdt['amount'] log_text = r'Applying fee on amount for .*'
if has_rounding_fee: if has_rounding_fee:
assert log_has_re(r'Applying fee on amount for .*', caplog) assert pytest.approx(trade.amount) == 29.992
assert log_has_re(log_text, caplog)
else:
assert pytest.approx(trade.amount) == limit_buy_order_usdt['amount']
assert not log_has_re(log_text, caplog)
def test_update_trade_state_exception(mocker, default_conf_usdt, def test_update_trade_state_exception(mocker, default_conf_usdt,
@ -1762,7 +1790,7 @@ def test_update_trade_state_sell(default_conf_usdt, trades_for_order, limit_sell
fee_open=0.0025, fee_open=0.0025,
fee_close=0.0025, fee_close=0.0025,
open_date=arrow.utcnow().datetime, open_date=arrow.utcnow().datetime,
open_order_id="123456", open_order_id=limit_sell_order_usdt_open['id'],
is_open=True, is_open=True,
) )
order = Order.parse_from_ccxt_object(limit_sell_order_usdt_open, 'LTC/ETH', 'sell') order = Order.parse_from_ccxt_object(limit_sell_order_usdt_open, 'LTC/ETH', 'sell')
@ -1803,7 +1831,8 @@ def test_handle_trade(default_conf_usdt, limit_buy_order_usdt, limit_sell_order_
assert trade assert trade
time.sleep(0.01) # Race condition fix time.sleep(0.01) # Race condition fix
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy')
trade.update_trade(oobj)
assert trade.is_open is True assert trade.is_open is True
freqtrade.wallets.update() freqtrade.wallets.update()
@ -1812,7 +1841,9 @@ def test_handle_trade(default_conf_usdt, limit_buy_order_usdt, limit_sell_order_
assert trade.open_order_id == limit_sell_order_usdt['id'] assert trade.open_order_id == limit_sell_order_usdt['id']
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order_usdt) oobj = Order.parse_from_ccxt_object(
limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell')
trade.update_trade(oobj)
assert trade.close_rate == 2.2 assert trade.close_rate == 2.2
assert trade.close_profit == 0.09451372 assert trade.close_profit == 0.09451372
@ -1962,8 +1993,11 @@ def test_close_trade(default_conf_usdt, ticker_usdt, limit_buy_order_usdt,
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy')
trade.update(limit_sell_order_usdt) trade.update_trade(oobj)
oobj = Order.parse_from_ccxt_object(
limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell')
trade.update_trade(oobj)
assert trade.is_open is False assert trade.is_open is False
with pytest.raises(DependencyException, match=r'.*closed trade.*'): with pytest.raises(DependencyException, match=r'.*closed trade.*'):
@ -1986,7 +2020,7 @@ def test_bot_loop_start_called_once(mocker, default_conf_usdt, caplog):
def test_check_handle_timedout_buy_usercustom(default_conf_usdt, ticker_usdt, limit_buy_order_old, def test_check_handle_timedout_buy_usercustom(default_conf_usdt, ticker_usdt, limit_buy_order_old,
open_trade, fee, mocker) -> None: open_trade, fee, mocker) -> None:
default_conf_usdt["unfilledtimeout"] = {"buy": 1400, "sell": 30} default_conf_usdt["unfilledtimeout"] = {"buy": 1400, "sell": 30}
limit_buy_order_old['id'] = open_trade.open_order_id
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock(return_value=limit_buy_order_old) cancel_order_mock = MagicMock(return_value=limit_buy_order_old)
cancel_buy_order = deepcopy(limit_buy_order_old) cancel_buy_order = deepcopy(limit_buy_order_old)
@ -2042,6 +2076,7 @@ def test_check_handle_timedout_buy_usercustom(default_conf_usdt, ticker_usdt, li
def test_check_handle_timedout_buy(default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade, def test_check_handle_timedout_buy(default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade,
fee, mocker) -> None: fee, mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
limit_buy_order_old['id'] = open_trade.open_order_id
limit_buy_cancel = deepcopy(limit_buy_order_old) limit_buy_cancel = deepcopy(limit_buy_order_old)
limit_buy_cancel['status'] = 'canceled' limit_buy_cancel['status'] = 'canceled'
cancel_order_mock = MagicMock(return_value=limit_buy_cancel) cancel_order_mock = MagicMock(return_value=limit_buy_cancel)
@ -2126,6 +2161,8 @@ def test_check_handle_timedout_buy_exception(default_conf_usdt, ticker_usdt,
def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, limit_sell_order_old, def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, limit_sell_order_old,
mocker, open_trade, caplog) -> None: mocker, open_trade, caplog) -> None:
default_conf_usdt["unfilledtimeout"] = {"buy": 1440, "sell": 1440, "exit_timeout_count": 1} default_conf_usdt["unfilledtimeout"] = {"buy": 1440, "sell": 1440, "exit_timeout_count": 1}
limit_sell_order_old['id'] = open_trade.open_order_id
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
patch_exchange(mocker) patch_exchange(mocker)
@ -2174,7 +2211,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l
# 2nd canceled trade - Fail execute sell # 2nd canceled trade - Fail execute sell
caplog.clear() caplog.clear()
open_trade.open_order_id = 'order_id_2' open_trade.open_order_id = limit_sell_order_old['id']
mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1) mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit',
side_effect=DependencyException) side_effect=DependencyException)
@ -2185,7 +2222,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l
caplog.clear() caplog.clear()
# 2nd canceled trade ... # 2nd canceled trade ...
open_trade.open_order_id = 'order_id_2' open_trade.open_order_id = limit_sell_order_old['id']
freqtrade.check_handle_timedout() freqtrade.check_handle_timedout()
assert log_has_re('Emergencyselling trade.*', caplog) assert log_has_re('Emergencyselling trade.*', caplog)
assert et_mock.call_count == 1 assert et_mock.call_count == 1
@ -2195,6 +2232,7 @@ def test_check_handle_timedout_sell(default_conf_usdt, ticker_usdt, limit_sell_o
open_trade) -> None: open_trade) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
limit_sell_order_old['id'] = open_trade.open_order_id
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
@ -2253,6 +2291,7 @@ def test_check_handle_cancelled_sell(default_conf_usdt, ticker_usdt, limit_sell_
def test_check_handle_timedout_partial(default_conf_usdt, ticker_usdt, limit_buy_order_old_partial, def test_check_handle_timedout_partial(default_conf_usdt, ticker_usdt, limit_buy_order_old_partial,
open_trade, mocker) -> None: open_trade, mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
limit_buy_order_old_partial['id'] = open_trade.open_order_id
limit_buy_canceled = deepcopy(limit_buy_order_old_partial) limit_buy_canceled = deepcopy(limit_buy_order_old_partial)
limit_buy_canceled['status'] = 'canceled' limit_buy_canceled['status'] = 'canceled'
@ -2283,6 +2322,8 @@ def test_check_handle_timedout_partial_fee(default_conf_usdt, ticker_usdt, open_
limit_buy_order_old_partial, trades_for_order, limit_buy_order_old_partial, trades_for_order,
limit_buy_order_old_partial_canceled, mocker) -> None: limit_buy_order_old_partial_canceled, mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
limit_buy_order_old_partial['id'] = open_trade.open_order_id
limit_buy_order_old_partial_canceled['id'] = open_trade.open_order_id
cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled) cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled)
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=0)) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=0))
patch_exchange(mocker) patch_exchange(mocker)
@ -2322,6 +2363,8 @@ def test_check_handle_timedout_partial_except(default_conf_usdt, ticker_usdt, op
fee, limit_buy_order_old_partial, trades_for_order, fee, limit_buy_order_old_partial, trades_for_order,
limit_buy_order_old_partial_canceled, mocker) -> None: limit_buy_order_old_partial_canceled, mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
limit_buy_order_old_partial_canceled['id'] = open_trade.open_order_id
limit_buy_order_old_partial['id'] = open_trade.open_order_id
cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled) cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
@ -2428,6 +2471,9 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_buy_order_
mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock) mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason) assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason)
assert log_has_re(r"Order .* for .* not cancelled.", caplog) assert log_has_re(r"Order .* for .* not cancelled.", caplog)
# min_pair_stake empty should not crash
mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=None)
assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason)
@pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'], @pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'],
@ -3092,7 +3138,8 @@ def test_sell_profit_only(
freqtrade.enter_positions() freqtrade.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy')
trade.update_trade(oobj)
freqtrade.wallets.update() freqtrade.wallets.update()
patch_get_signal(freqtrade, value=(False, True, None, None)) patch_get_signal(freqtrade, value=(False, True, None, None))
assert freqtrade.handle_trade(trade) is handle_first assert freqtrade.handle_trade(trade) is handle_first
@ -3128,7 +3175,9 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_buy_order_usdt, limit_
trade = Trade.query.first() trade = Trade.query.first()
amnt = trade.amount amnt = trade.amount
trade.update(limit_buy_order_usdt)
oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy')
trade.update_trade(oobj)
patch_get_signal(freqtrade, value=(False, True, None, None)) patch_get_signal(freqtrade, value=(False, True, None, None))
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985)) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985))
@ -3236,7 +3285,8 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usdt,
freqtrade.enter_positions() freqtrade.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy')
trade.update_trade(oobj)
freqtrade.wallets.update() freqtrade.wallets.update()
patch_get_signal(freqtrade, value=(True, True, None, None)) patch_get_signal(freqtrade, value=(True, True, None, None))
assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_trade(trade) is False
@ -3339,7 +3389,8 @@ def test_trailing_stop_loss_positive(
freqtrade.enter_positions() freqtrade.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy')
trade.update_trade(oobj)
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
# stop-loss not reached # stop-loss not reached
assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_trade(trade) is False
@ -3426,7 +3477,8 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usd
freqtrade.enter_positions() freqtrade.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy')
trade.update_trade(oobj)
# Sell due to min_roi_reached # Sell due to min_roi_reached
patch_get_signal(freqtrade, value=(True, False, None, None)) patch_get_signal(freqtrade, value=(True, False, None, None))
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
@ -3801,7 +3853,8 @@ def test_order_book_depth_of_market(
assert len(Trade.query.all()) == 1 assert len(Trade.query.all()) == 1
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
assert trade.open_rate == 2.0 assert trade.open_rate == 2.0
assert whitelist == default_conf_usdt['exchange']['pair_whitelist'] assert whitelist == default_conf_usdt['exchange']['pair_whitelist']
@ -3895,7 +3948,8 @@ def test_order_book_ask_strategy(
assert trade assert trade
time.sleep(0.01) # Race condition fix time.sleep(0.01) # Race condition fix
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy')
trade.update_trade(oobj)
freqtrade.wallets.update() freqtrade.wallets.update()
assert trade.is_open is True assert trade.is_open is True
@ -4102,15 +4156,17 @@ def test_reupdate_enter_order_fees(mocker, default_conf_usdt, fee, caplog):
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state')
mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
return_value={'status': 'open'})
create_mock_trades(fee) create_mock_trades(fee)
trades = Trade.get_trades().all() trades = Trade.get_trades().all()
freqtrade.reupdate_enter_order_fees(trades[0]) freqtrade.handle_insufficient_funds(trades[3])
assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) # assert log_has_re(r"Trying to reupdate buy fees for .*", caplog)
assert mock_uts.call_count == 1 assert mock_uts.call_count == 1
assert mock_uts.call_args_list[0][0][0] == trades[0] assert mock_uts.call_args_list[0][0][0] == trades[3]
assert mock_uts.call_args_list[0][0][1] == mock_order_1()['id'] assert mock_uts.call_args_list[0][0][1] == mock_order_4()['id']
assert log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) assert log_has_re(r"Trying to refind lost order for .*", caplog)
mock_uts.reset_mock() mock_uts.reset_mock()
caplog.clear() caplog.clear()
@ -4128,52 +4184,13 @@ def test_reupdate_enter_order_fees(mocker, default_conf_usdt, fee, caplog):
) )
Trade.query.session.add(trade) Trade.query.session.add(trade)
freqtrade.reupdate_enter_order_fees(trade) freqtrade.handle_insufficient_funds(trade)
assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) # assert log_has_re(r"Trying to reupdate buy fees for .*", caplog)
assert mock_uts.call_count == 0 assert mock_uts.call_count == 0
assert not log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog)
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_handle_insufficient_funds(mocker, default_conf_usdt, fee): def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, caplog):
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order')
mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_enter_order_fees')
create_mock_trades(fee)
trades = Trade.get_trades().all()
# Trade 0 has only a open buy order, no closed order
freqtrade.handle_insufficient_funds(trades[0])
assert mock_rlo.call_count == 0
assert mock_bof.call_count == 1
mock_rlo.reset_mock()
mock_bof.reset_mock()
# Trade 1 has closed buy and sell orders
freqtrade.handle_insufficient_funds(trades[1])
assert mock_rlo.call_count == 1
assert mock_bof.call_count == 0
mock_rlo.reset_mock()
mock_bof.reset_mock()
# Trade 2 has closed buy and sell orders
freqtrade.handle_insufficient_funds(trades[2])
assert mock_rlo.call_count == 1
assert mock_bof.call_count == 0
mock_rlo.reset_mock()
mock_bof.reset_mock()
# Trade 3 has an opne buy order
freqtrade.handle_insufficient_funds(trades[3])
assert mock_rlo.call_count == 0
assert mock_bof.call_count == 1
@pytest.mark.usefixtures("init_persistence")
def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog):
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state')
@ -4196,7 +4213,7 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog):
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
freqtrade.refind_lost_order(trade) freqtrade.handle_insufficient_funds(trade)
order = mock_order_1() order = mock_order_1()
assert log_has_re(r"Order Order(.*order_id=" + order['id'] + ".*) is no longer open.", caplog) assert log_has_re(r"Order Order(.*order_id=" + order['id'] + ".*) is no longer open.", caplog)
assert mock_fo.call_count == 0 assert mock_fo.call_count == 0
@ -4214,13 +4231,13 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog):
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
freqtrade.refind_lost_order(trade) freqtrade.handle_insufficient_funds(trade)
order = mock_order_4() order = mock_order_4()
assert log_has_re(r"Trying to refind Order\(.*", caplog) assert log_has_re(r"Trying to refind Order\(.*", caplog)
assert mock_fo.call_count == 0 assert mock_fo.call_count == 1
assert mock_uts.call_count == 0 assert mock_uts.call_count == 1
# No change to orderid - as update_trade_state is mocked # Found open buy order
assert trade.open_order_id is None assert trade.open_order_id is not None
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
caplog.clear() caplog.clear()
@ -4232,11 +4249,11 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog):
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
freqtrade.refind_lost_order(trade) freqtrade.handle_insufficient_funds(trade)
order = mock_order_5_stoploss() order = mock_order_5_stoploss()
assert log_has_re(r"Trying to refind Order\(.*", caplog) assert log_has_re(r"Trying to refind Order\(.*", caplog)
assert mock_fo.call_count == 1 assert mock_fo.call_count == 1
assert mock_uts.call_count == 1 assert mock_uts.call_count == 2
# stoploss_order_id is "refound" and added to the trade # stoploss_order_id is "refound" and added to the trade
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.stoploss_order_id is not None assert trade.stoploss_order_id is not None
@ -4251,7 +4268,7 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog):
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
freqtrade.refind_lost_order(trade) freqtrade.handle_insufficient_funds(trade)
order = mock_order_6_sell() order = mock_order_6_sell()
assert log_has_re(r"Trying to refind Order\(.*", caplog) assert log_has_re(r"Trying to refind Order\(.*", caplog)
assert mock_fo.call_count == 1 assert mock_fo.call_count == 1
@ -4267,7 +4284,7 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog):
side_effect=ExchangeError()) side_effect=ExchangeError())
order = mock_order_5_stoploss() order = mock_order_5_stoploss()
freqtrade.refind_lost_order(trades[4]) freqtrade.handle_insufficient_funds(trades[4])
assert log_has(f"Error updating {order['id']}.", caplog) assert log_has(f"Error updating {order['id']}.", caplog)

View File

@ -4,6 +4,7 @@ import pytest
from freqtrade.enums import SellType from freqtrade.enums import SellType
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.persistence.models import Order
from freqtrade.rpc.rpc import RPC from freqtrade.rpc.rpc import RPC
from freqtrade.strategy.interface import SellCheckTuple from freqtrade.strategy.interface import SellCheckTuple
from tests.conftest import get_patched_freqtradebot, patch_get_signal from tests.conftest import get_patched_freqtradebot, patch_get_signal
@ -94,7 +95,11 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
trades = Trade.query.all() trades = Trade.query.all()
# Make sure stoploss-order is open and trade is bought (since we mock update_trade_state) # Make sure stoploss-order is open and trade is bought (since we mock update_trade_state)
for trade in trades: for trade in trades:
trade.stoploss_order_id = 3 stoploss_order_closed['id'] = '3'
oobj = Order.parse_from_ccxt_object(stoploss_order_closed, trade.pair, 'stoploss')
trade.orders.append(oobj)
trade.stoploss_order_id = '3'
trade.open_order_id = None trade.open_order_id = None
n = freqtrade.exit_positions(trades) n = freqtrade.exit_positions(trades)

View File

@ -21,16 +21,19 @@ def test_decimals_per_coin():
def test_round_coin_value(): def test_round_coin_value():
assert round_coin_value(222.222222, 'USDT') == '222.222 USDT' assert round_coin_value(222.222222, 'USDT') == '222.222 USDT'
assert round_coin_value(222.2, 'USDT') == '222.200 USDT' assert round_coin_value(222.2, 'USDT', keep_trailing_zeros=True) == '222.200 USDT'
assert round_coin_value(222.2, 'USDT') == '222.2 USDT'
assert round_coin_value(222.12745, 'EUR') == '222.127 EUR' assert round_coin_value(222.12745, 'EUR') == '222.127 EUR'
assert round_coin_value(0.1274512123, 'BTC') == '0.12745121 BTC' assert round_coin_value(0.1274512123, 'BTC') == '0.12745121 BTC'
assert round_coin_value(0.1274512123, 'ETH') == '0.12745 ETH' assert round_coin_value(0.1274512123, 'ETH') == '0.12745 ETH'
assert round_coin_value(222.222222, 'USDT', False) == '222.222' assert round_coin_value(222.222222, 'USDT', False) == '222.222'
assert round_coin_value(222.2, 'USDT', False) == '222.200' assert round_coin_value(222.2, 'USDT', False) == '222.2'
assert round_coin_value(222.00, 'USDT', False) == '222'
assert round_coin_value(222.12745, 'EUR', False) == '222.127' assert round_coin_value(222.12745, 'EUR', False) == '222.127'
assert round_coin_value(0.1274512123, 'BTC', False) == '0.12745121' assert round_coin_value(0.1274512123, 'BTC', False) == '0.12745121'
assert round_coin_value(0.1274512123, 'ETH', False) == '0.12745' assert round_coin_value(0.1274512123, 'ETH', False) == '0.12745'
assert round_coin_value(222.2, 'USDT', False, True) == '222.200'
def test_shorten_date() -> None: def test_shorten_date() -> None:

View File

@ -8,11 +8,12 @@ from unittest.mock import MagicMock
import arrow import arrow
import pytest import pytest
from sqlalchemy import create_engine, inspect, text from sqlalchemy import create_engine, text
from freqtrade import constants from freqtrade import constants
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db
from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids
from tests.conftest import create_mock_trades, create_mock_trades_usdt, log_has, log_has_re from tests.conftest import create_mock_trades, create_mock_trades_usdt, log_has, log_has_re
@ -32,13 +33,17 @@ def test_init_custom_db_url(default_conf, tmpdir):
init_db(default_conf['db_url'], default_conf['dry_run']) init_db(default_conf['db_url'], default_conf['dry_run'])
assert Path(filename).is_file() assert Path(filename).is_file()
r = Trade._session.execute(text("PRAGMA journal_mode"))
assert r.first() == ('wal',)
def test_init_invalid_db_url(default_conf): def test_init_invalid_db_url():
# Update path to a value other than default, but still in-memory # Update path to a value other than default, but still in-memory
default_conf.update({'db_url': 'unknown:///some.url'})
with pytest.raises(OperationalException, match=r'.*no valid database URL*'): with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
init_db(default_conf['db_url'], default_conf['dry_run']) init_db('unknown:///some.url', True)
with pytest.raises(OperationalException, match=r'Bad db-url.*For in-memory database, pl.*'):
init_db('sqlite:///', True)
def test_init_prod_db(default_conf, mocker): def test_init_prod_db(default_conf, mocker):
@ -107,7 +112,8 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca
assert trade.close_date is None assert trade.close_date is None
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.open_rate == 2.00 assert trade.open_rate == 2.00
assert trade.close_profit is None assert trade.close_profit is None
@ -118,7 +124,8 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca
caplog.clear() caplog.clear()
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_sell_order_usdt) oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell')
trade.update_trade(oobj)
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.close_rate == 2.20 assert trade.close_rate == 2.20
assert trade.close_profit == round(0.0945137157107232, 8) assert trade.close_profit == round(0.0945137157107232, 8)
@ -145,7 +152,8 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
) )
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(market_buy_order_usdt) oobj = Order.parse_from_ccxt_object(market_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.open_rate == 2.0 assert trade.open_rate == 2.0
assert trade.close_profit is None assert trade.close_profit is None
@ -157,7 +165,8 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
caplog.clear() caplog.clear()
trade.is_open = True trade.is_open = True
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(market_sell_order_usdt) oobj = Order.parse_from_ccxt_object(market_sell_order_usdt, 'ADA/USDT', 'sell')
trade.update_trade(oobj)
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.close_rate == 2.2 assert trade.close_rate == 2.2
assert trade.close_profit == round(0.0945137157107232, 8) assert trade.close_profit == round(0.0945137157107232, 8)
@ -180,9 +189,11 @@ def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt
) )
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
assert trade._calc_open_trade_value() == 60.15 assert trade._calc_open_trade_value() == 60.15
trade.update(limit_sell_order_usdt) oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell')
trade.update_trade(oobj)
assert isclose(trade.calc_close_trade_value(), 65.835) assert isclose(trade.calc_close_trade_value(), 65.835)
# Profit in USDT # Profit in USDT
@ -235,7 +246,8 @@ def test_calc_close_trade_price_exception(limit_buy_order_usdt, fee):
) )
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
assert trade.calc_close_trade_value() == 0.0 assert trade.calc_close_trade_value() == 0.0
@ -256,7 +268,8 @@ def test_update_open_order(limit_buy_order_usdt):
assert trade.close_date is None assert trade.close_date is None
limit_buy_order_usdt['status'] = 'open' limit_buy_order_usdt['status'] = 'open'
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.close_profit is None assert trade.close_profit is None
@ -275,8 +288,9 @@ def test_update_invalid_order(limit_buy_order_usdt):
exchange='binance', exchange='binance',
) )
limit_buy_order_usdt['type'] = 'invalid' limit_buy_order_usdt['type'] = 'invalid'
oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'meep')
with pytest.raises(ValueError, match=r'Unknown order type'): with pytest.raises(ValueError, match=r'Unknown order type'):
trade.update(limit_buy_order_usdt) trade.update_trade(oobj)
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@ -303,7 +317,8 @@ def test_calc_open_trade_value(limit_buy_order_usdt, fee):
exchange='binance', exchange='binance',
) )
trade.open_order_id = 'open_trade' trade.open_order_id = 'open_trade'
trade.update(limit_buy_order_usdt) # Buy @ 2.0 oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj) # Buy @ 2.0
# Get the open rate price with the standard fee rate # Get the open rate price with the standard fee rate
assert trade._calc_open_trade_value() == 60.15 assert trade._calc_open_trade_value() == 60.15
@ -324,14 +339,16 @@ def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee
exchange='binance', exchange='binance',
) )
trade.open_order_id = 'close_trade' trade.open_order_id = 'close_trade'
trade.update(limit_buy_order_usdt) # Buy @ 2.0 oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj) # Buy @ 2.0
# Get the close rate price with a custom close rate and a regular fee rate # Get the close rate price with a custom close rate and a regular fee rate
assert trade.calc_close_trade_value(rate=2.5) == 74.8125 assert trade.calc_close_trade_value(rate=2.5) == 74.8125
# Get the close rate price with a custom close rate and a custom fee rate # Get the close rate price with a custom close rate and a custom fee rate
assert trade.calc_close_trade_value(rate=2.5, fee=0.003) == 74.775 assert trade.calc_close_trade_value(rate=2.5, fee=0.003) == 74.775
# Test when we apply a Sell order, and ask price with a custom fee rate # Test when we apply a Sell order, and ask price with a custom fee rate
trade.update(limit_sell_order_usdt) oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell')
trade.update_trade(oobj)
assert trade.calc_close_trade_value(fee=0.005) == 65.67 assert trade.calc_close_trade_value(fee=0.005) == 65.67
@ -408,7 +425,9 @@ def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee):
exchange='binance', exchange='binance',
) )
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_buy_order_usdt) # Buy @ 2.0 oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj) # Buy @ 2.0
# Custom closing rate and regular fee rate # Custom closing rate and regular fee rate
# Higher than open rate - 2.1 quote # Higher than open rate - 2.1 quote
@ -423,7 +442,8 @@ def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee):
assert trade.calc_profit(rate=1.9, fee=0.003) == round(-3.320999999999998, 8) assert trade.calc_profit(rate=1.9, fee=0.003) == round(-3.320999999999998, 8)
# Test when we apply a Sell order. Sell higher than open rate @ 2.2 # Test when we apply a Sell order. Sell higher than open rate @ 2.2
trade.update(limit_sell_order_usdt) oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell')
trade.update_trade(oobj)
assert trade.calc_profit() == round(5.684999999999995, 8) assert trade.calc_profit() == round(5.684999999999995, 8)
# Test with a custom fee rate on the close trade # Test with a custom fee rate on the close trade
@ -442,7 +462,9 @@ def test_calc_profit_ratio(limit_buy_order_usdt, limit_sell_order_usdt, fee):
exchange='binance' exchange='binance'
) )
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_buy_order_usdt) # Buy @ 2.0
oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj) # Buy @ 2.0
# Higher than open rate - 2.1 quote # Higher than open rate - 2.1 quote
assert trade.calc_profit_ratio(rate=2.1) == round(0.04476309226932673, 8) assert trade.calc_profit_ratio(rate=2.1) == round(0.04476309226932673, 8)
@ -456,7 +478,8 @@ def test_calc_profit_ratio(limit_buy_order_usdt, limit_sell_order_usdt, fee):
assert trade.calc_profit_ratio(rate=1.9, fee=0.003) == round(-0.05521197007481293, 8) assert trade.calc_profit_ratio(rate=1.9, fee=0.003) == round(-0.05521197007481293, 8)
# Test when we apply a Sell order. Sell higher than open rate @ 2.2 # Test when we apply a Sell order. Sell higher than open rate @ 2.2
trade.update(limit_sell_order_usdt) oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell')
trade.update_trade(oobj)
assert trade.calc_profit_ratio() == round(0.0945137157107232, 8) assert trade.calc_profit_ratio() == round(0.0945137157107232, 8)
# Test with a custom fee rate on the close trade # Test with a custom fee rate on the close trade
@ -600,7 +623,8 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert trade.stoploss_last_update is None assert trade.stoploss_last_update is None
assert log_has("trying trades_bak1", caplog) assert log_has("trying trades_bak1", caplog)
assert log_has("trying trades_bak2", caplog) assert log_has("trying trades_bak2", caplog)
assert log_has("Running database migration for trades - backup: trades_bak2", caplog) assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0",
caplog)
assert trade.open_trade_value == trade._calc_open_trade_value() assert trade.open_trade_value == trade._calc_open_trade_value()
assert trade.close_profit_abs is None assert trade.close_profit_abs is None
@ -613,65 +637,6 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert orders[1].order_id == 'stop_order_id222' assert orders[1].order_id == 'stop_order_id222'
assert orders[1].ft_order_side == 'stoploss' assert orders[1].ft_order_side == 'stoploss'
caplog.clear()
# Drop latest column
with engine.begin() as connection:
connection.execute(text("alter table orders rename to orders_bak"))
inspector = inspect(engine)
with engine.begin() as connection:
for index in inspector.get_indexes('orders_bak'):
connection.execute(text(f"drop index {index['name']}"))
# Recreate table
connection.execute(text("""
CREATE TABLE orders (
id INTEGER NOT NULL,
ft_trade_id INTEGER,
ft_order_side VARCHAR NOT NULL,
ft_pair VARCHAR NOT NULL,
ft_is_open BOOLEAN NOT NULL,
order_id VARCHAR NOT NULL,
status VARCHAR,
symbol VARCHAR,
order_type VARCHAR,
side VARCHAR,
price FLOAT,
amount FLOAT,
filled FLOAT,
remaining FLOAT,
cost FLOAT,
order_date DATETIME,
order_filled_date DATETIME,
order_update_date DATETIME,
PRIMARY KEY (id),
CONSTRAINT _order_pair_order_id UNIQUE (ft_pair, order_id),
FOREIGN KEY(ft_trade_id) REFERENCES trades (id)
)
"""))
connection.execute(text("""
insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status,
symbol, order_type, side, price, amount, filled, remaining, cost, order_date,
order_filled_date, order_update_date)
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status,
symbol, order_type, side, price, amount, filled, remaining, cost, order_date,
order_filled_date, order_update_date
from orders_bak
"""))
# Run init to test migration
init_db(default_conf['db_url'], default_conf['dry_run'])
assert log_has("trying orders_bak1", caplog)
orders = Order.query.all()
assert len(orders) == 2
assert orders[0].order_id == 'buy_order'
assert orders[0].ft_order_side == 'buy'
assert orders[1].order_id == 'stop_order_id222'
assert orders[1].ft_order_side == 'stoploss'
def test_migrate_mid_state(mocker, default_conf, fee, caplog): def test_migrate_mid_state(mocker, default_conf, fee, caplog):
""" """
@ -733,7 +698,40 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
assert trade.initial_stop_loss == 0.0 assert trade.initial_stop_loss == 0.0
assert trade.open_trade_value == trade._calc_open_trade_value() assert trade.open_trade_value == trade._calc_open_trade_value()
assert log_has("trying trades_bak0", caplog) assert log_has("trying trades_bak0", caplog)
assert log_has("Running database migration for trades - backup: trades_bak0", caplog) assert log_has("Running database migration for trades - backup: trades_bak0, orders_bak0",
caplog)
def test_migrate_get_last_sequence_ids():
engine = MagicMock()
engine.begin = MagicMock()
engine.name = 'postgresql'
get_last_sequence_ids(engine, 'trades_bak', 'orders_bak')
assert engine.begin.call_count == 2
engine.reset_mock()
engine.begin.reset_mock()
engine.name = 'somethingelse'
get_last_sequence_ids(engine, 'trades_bak', 'orders_bak')
assert engine.begin.call_count == 0
def test_migrate_set_sequence_ids():
engine = MagicMock()
engine.begin = MagicMock()
engine.name = 'postgresql'
set_sequence_ids(engine, 22, 55)
assert engine.begin.call_count == 1
engine.reset_mock()
engine.begin.reset_mock()
engine.name = 'somethingelse'
set_sequence_ids(engine, 22, 55)
assert engine.begin.call_count == 0
def test_adjust_stop_loss(fee): def test_adjust_stop_loss(fee):
@ -903,6 +901,8 @@ def test_to_json(default_conf, fee):
'buy_tag': None, 'buy_tag': None,
'timeframe': None, 'timeframe': None,
'exchange': 'binance', 'exchange': 'binance',
'filled_entry_orders': [],
'filled_exit_orders': []
} }
# Simulate dry_run entries # Simulate dry_run entries
@ -970,6 +970,8 @@ def test_to_json(default_conf, fee):
'buy_tag': 'buys_signal_001', 'buy_tag': 'buys_signal_001',
'timeframe': None, 'timeframe': None,
'exchange': 'binance', 'exchange': 'binance',
'filled_entry_orders': [],
'filled_exit_orders': []
} }
@ -1297,11 +1299,14 @@ def test_select_order(fee):
order = trades[4].select_order('buy', False) order = trades[4].select_order('buy', False)
assert order is not None assert order is not None
trades[4].orders[1].ft_order_side = 'sell'
order = trades[4].select_order('sell', True) order = trades[4].select_order('sell', True)
assert order is not None assert order is not None
trades[4].orders[1].ft_order_side = 'stoploss'
order = trades[4].select_order('stoploss', None)
assert order is not None
assert order.ft_order_side == 'stoploss' assert order.ft_order_side == 'stoploss'
order = trades[4].select_order('sell', False)
assert order is None
def test_Trade_object_idem(): def test_Trade_object_idem():